API Reference
Base URL: https://api.quolle.com · All requests and responses use JSON.
Authentication
Auth endpoints return a JWT token used for dashboard requests. For API/SMTP access, use an API key.
Create a new account. Returns a JWT token.
| Field | Type | Required | Description |
|---|---|---|---|
name | string | required | Full name |
email | string | required | Email address |
password | string | required | Minimum 8 characters |
// Response 201
{
"token": "eyJhbGciOiJIUzI1NiJ9...",
"customer": { "id": "uuid", "name": "Amara", "email": "amara@example.com" }
}
Authenticate and receive a JWT token.
| Field | Type | Required | Description |
|---|---|---|---|
email | string | required | Registered email |
password | string | required | Account password |
// Response 200
{
"token": "eyJhbGciOiJIUzI1NiJ9...",
"customer": { "id": "uuid", "name": "Amara", "email": "amara@example.com" }
}
Emails
All email endpoints require an API key in the Authorization: Bearer header.
Idempotency keys
Add an Idempotency-Key header to any POST /send or
POST /batch request to make it safe to retry. If a request with the same key
(scoped to your account) has already succeeded, the cached response is returned immediately
with an Idempotency-Replay: true header. Keys expire after 24 hours.
curl -X POST https://api.quolle.com/v1/emails/send \
-H "Authorization: Bearer qle_your_api_key" \
-H "Idempotency-Key: order_invoice_12345" \
-H "Content-Type: application/json" \
-d '{ "from": "…", "to": "…", "subject": "…", "html": "…" }'
Send a single transactional email.
Idempotency-Key| Field | Type | Required | Description |
|---|---|---|---|
from | string | required | Verified sender address |
to | string | required | Recipient email address |
subject | string | required | Email subject line |
html | string | optional* | HTML body (* html or text required) |
text | string | optional* | Plain-text body (auto-generated from html if omitted) |
replyTo | string | optional | Reply-to address |
scheduledAt | string | optional | ISO 8601 datetime to delay delivery (e.g. 2026-12-25T09:00:00.000Z) |
metadata | object | optional | Arbitrary JSON metadata stored with the email |
// Response 202
{
"id": "a1b2c3d4-e5f6-...",
"status": "queued", // or "scheduled" when scheduledAt is set
"message": "Email queued successfully",
"scheduledAt": null // ISO datetime if scheduled, else null
}
Send up to 50 emails in a single request. Each email is queued independently.
| Field | Type | Required | Description |
|---|---|---|---|
emails | array | required | Array of email objects (same shape as /send, max 50) |
// Response 202
{
"queued": 3,
"ids": ["uuid-1", "uuid-2", "uuid-3"]
}
Get status and details for a specific email.
// Response 200
{
"id": "a1b2c3d4-...",
"from": "hello@mail.yourdomain.com",
"to": "customer@example.com",
"subject": "Welcome!",
"status": "delivered",
"provider": "ses",
"sentAt": "2026-06-19T10:00:00.000Z",
"deliveredAt": "2026-06-19T10:00:02.000Z",
"createdAt": "2026-06-19T10:00:00.000Z"
}
Domains
List all domains added to your account.
// Response 200
{
"domains": [
{
"id": "uuid",
"domain": "mail.yourdomain.com",
"status": "verified",
"dnsProvider": "cloudflare",
"createdAt": "2026-06-01T00:00:00.000Z"
}
]
}
Add a new domain. Returns DNS records to publish for verification.
| Field | Type | Required | Description |
|---|---|---|---|
domain | string | required | Valid hostname, e.g. mail.yourdomain.com |
// Response 201
{
"domain": { "id": "uuid", "domain": "mail.yourdomain.com", "status": "pending" },
"dnsRecords": [
{ "type": "TXT", "name": "_amazonses.mail.yourdomain.com", "value": "abc123..." },
{ "type": "TXT", "name": "mail.yourdomain.com", "value": "v=spf1 include:amazonses.com ~all" },
{ "type": "CNAME", "name": "token1._domainkey.mail.yourdomain.com", "value": "token1.dkim.amazonses.com" }
],
"isCloudflare": true,
"message": "Add these DNS records to verify your domain"
}
Check whether SES has confirmed your DNS records. Call this after publishing DNS records.
// Response 200
{ "verified": true, "status": "verified", "message": "Domain verified successfully" }
// or if not yet verified:
{ "verified": false, "status": "pending", "message": "Domain not yet verified. Ensure DNS records are published and retry." }
Remove a domain from your account.
// Response 200
{ "message": "Domain removed" }
API Keys
List all API keys (previews only — full key is never returned after creation).
// Response 200
{
"keys": [
{
"id": "uuid",
"name": "Production",
"preview": "qle_****8f3a",
"lastUsedAt": "2026-06-19T10:00:00.000Z",
"createdAt": "2026-06-01T00:00:00.000Z"
}
],
"smtp": {
"host": "smtp.quolle.com",
"port": 587,
"security": "STARTTLS",
"note": "Use your API key as both username and password"
}
}
Create a new API key. The full key is returned only in this response — store it immediately.
| Field | Type | Required | Description |
|---|---|---|---|
name | string | required | Descriptive label (e.g. "Production", "Staging") |
// Response 201
{
"key": "qle_a1b2c3d4e5f6...",
"id": "uuid",
"name": "Production",
"preview": "qle_****e5f6"
}
Revoke and delete an API key. Any requests using this key will immediately fail.
// Response 200
{ "message": "API key deleted" }
Usage
Get usage statistics for your account — current month and 6-month history.
// Response 200
{
"current": {
"sent": 842,
"limit": 3000,
"percentage": 28
},
"history": [
{ "month": "2026-01", "sent": 521 },
{ "month": "2026-02", "sent": 634 }
],
"plan": { "name": "Starter", "monthlyLimit": 3000 }
}
Webhooks
List all configured webhook endpoints.
// Response 200
{
"webhooks": [
{
"id": "uuid",
"url": "https://yourapp.com/webhooks/email",
"events": ["email.delivered", "email.bounced"],
"createdAt": "2026-06-01T00:00:00.000Z"
}
]
}
Create a webhook endpoint to receive delivery event notifications.
| Field | Type | Required | Description |
|---|---|---|---|
url | string | required | HTTPS URL to receive POST requests |
events | string[] | required | Array of event names to subscribe to |
// Request
{ "url": "https://yourapp.com/webhooks/email", "events": ["email.delivered", "email.bounced"] }
// Response 201
{ "id": "uuid", "url": "https://yourapp.com/webhooks/email", "events": ["email.delivered", "email.bounced"] }
Delete a webhook endpoint.
// Response 200
{ "message": "Webhook deleted" }
Billing
List all available plans with features and pricing in Naira. Public — no auth required.
// Response 200
{
"plans": [
{
"id": "uuid",
"name": "Starter",
"monthlyLimit": 3000,
"priceNaira": 0,
"features": ["3,000 emails/mo", "API access", "SMTP access", "1 domain"]
},
{
"id": "uuid",
"name": "Growth",
"monthlyLimit": 50000,
"priceNaira": 15000,
"features": ["50,000 emails/mo", "API access", "SMTP access", "5 domains", "Webhook support"]
}
]
}
Initiate a plan upgrade. For paid plans, returns a Paystack payment URL. For the free Starter plan, switches immediately.
| Field | Type | Required | Description |
|---|---|---|---|
planId | string (uuid) | required | ID of the plan to switch to |
// Paid plan — Response 200
{ "authorizationUrl": "https://checkout.paystack.com/...", "reference": "ref_abc123" }
// Free plan — Response 200
{ "message": "Switched to Starter plan" }
Get current subscription status, plan details, and usage for the authenticated customer.
// Response 200
{
"plan": { "id": "uuid", "name": "Growth", "monthlyLimit": 50000, "priceNaira": 15000 },
"status": "active",
"currentPeriodEnd": "2026-07-19T00:00:00.000Z",
"usage": { "sent": 1240, "limit": 50000, "percentage": 2 }
}
Rate limits
All authenticated endpoints are rate-limited to 100 requests per minute per API key.
Exceeding this returns a 429 Too Many Requests response.
| Header | Description |
|---|---|
X-RateLimit-Limit | 100 — maximum requests per window |
X-RateLimit-Remaining | Requests remaining in the current 60-second window |
X-RateLimit-Reset | Unix timestamp (seconds) when the window resets |
Error codes
| Status | Meaning |
|---|---|
400 | Bad Request — invalid or missing fields |
401 | Unauthorized — missing or invalid API key / JWT |
403 | Forbidden — you don't have access to this resource |
404 | Not Found — resource does not exist |
409 | Conflict — resource already exists (e.g. duplicate domain) |
422 | Unprocessable — monthly limit reached for your plan |
429 | Too Many Requests — rate limit exceeded |
500 | Server Error — something went wrong on our side |