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.

POST /v1/auth/register

Create a new account. Returns a JWT token.

FieldTypeRequiredDescription
namestringrequiredFull name
emailstringrequiredEmail address
passwordstringrequiredMinimum 8 characters
// Response 201
{
  "token": "eyJhbGciOiJIUzI1NiJ9...",
  "customer": { "id": "uuid", "name": "Amara", "email": "amara@example.com" }
}
POST /v1/auth/login

Authenticate and receive a JWT token.

FieldTypeRequiredDescription
emailstringrequiredRegistered email
passwordstringrequiredAccount 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": "…" }'
POST /v1/emails/send

Send a single transactional email.

🔑 Requires API key · Supports Idempotency-Key
FieldTypeRequiredDescription
fromstringrequiredVerified sender address
tostringrequiredRecipient email address
subjectstringrequiredEmail subject line
htmlstringoptional*HTML body (* html or text required)
textstringoptional*Plain-text body (auto-generated from html if omitted)
replyTostringoptionalReply-to address
scheduledAtstringoptionalISO 8601 datetime to delay delivery (e.g. 2026-12-25T09:00:00.000Z)
metadataobjectoptionalArbitrary 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
}
POST /v1/emails/batch

Send up to 50 emails in a single request. Each email is queued independently.

🔑 Requires API key
FieldTypeRequiredDescription
emailsarrayrequiredArray of email objects (same shape as /send, max 50)
// Response 202
{
  "queued": 3,
  "ids": ["uuid-1", "uuid-2", "uuid-3"]
}
GET /v1/emails/:id

Get status and details for a specific email.

🔑 Requires API key
// 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

GET /v1/domains

List all domains added to your account.

🔑 Requires JWT or API key
// Response 200
{
  "domains": [
    {
      "id": "uuid",
      "domain": "mail.yourdomain.com",
      "status": "verified",
      "dnsProvider": "cloudflare",
      "createdAt": "2026-06-01T00:00:00.000Z"
    }
  ]
}
POST /v1/domains

Add a new domain. Returns DNS records to publish for verification.

🔑 Requires JWT
FieldTypeRequiredDescription
domainstringrequiredValid 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"
}
POST /v1/domains/:id/verify

Check whether SES has confirmed your DNS records. Call this after publishing DNS records.

🔑 Requires JWT
// 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." }
DELETE /v1/domains/:id

Remove a domain from your account.

🔑 Requires JWT
// Response 200
{ "message": "Domain removed" }

API Keys

GET /v1/keys

List all API keys (previews only — full key is never returned after creation).

🔑 Requires JWT
// 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"
  }
}
POST /v1/keys

Create a new API key. The full key is returned only in this response — store it immediately.

🔑 Requires JWT
FieldTypeRequiredDescription
namestringrequiredDescriptive label (e.g. "Production", "Staging")
// Response 201
{
  "key": "qle_a1b2c3d4e5f6...",
  "id": "uuid",
  "name": "Production",
  "preview": "qle_****e5f6"
}
DELETE /v1/keys/:id

Revoke and delete an API key. Any requests using this key will immediately fail.

🔑 Requires JWT
// Response 200
{ "message": "API key deleted" }

Usage

GET /v1/usage

Get usage statistics for your account — current month and 6-month history.

🔑 Requires JWT or API key
// 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

GET /v1/webhooks

List all configured webhook endpoints.

🔑 Requires JWT
// Response 200
{
  "webhooks": [
    {
      "id": "uuid",
      "url": "https://yourapp.com/webhooks/email",
      "events": ["email.delivered", "email.bounced"],
      "createdAt": "2026-06-01T00:00:00.000Z"
    }
  ]
}
POST /v1/webhooks

Create a webhook endpoint to receive delivery event notifications.

🔑 Requires JWT
FieldTypeRequiredDescription
urlstringrequiredHTTPS URL to receive POST requests
eventsstring[]requiredArray 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 /v1/webhooks/:id

Delete a webhook endpoint.

🔑 Requires JWT
// Response 200
{ "message": "Webhook deleted" }

Billing

GET /v1/billing/plans

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"]
    }
  ]
}
POST /v1/billing/subscribe

Initiate a plan upgrade. For paid plans, returns a Paystack payment URL. For the free Starter plan, switches immediately.

🔑 Requires JWT
FieldTypeRequiredDescription
planIdstring (uuid)requiredID 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 /v1/billing/subscription

Get current subscription status, plan details, and usage for the authenticated customer.

🔑 Requires JWT
// 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.

HeaderDescription
X-RateLimit-Limit100 — maximum requests per window
X-RateLimit-RemainingRequests remaining in the current 60-second window
X-RateLimit-ResetUnix timestamp (seconds) when the window resets

Error codes

StatusMeaning
400Bad Request — invalid or missing fields
401Unauthorized — missing or invalid API key / JWT
403Forbidden — you don't have access to this resource
404Not Found — resource does not exist
409Conflict — resource already exists (e.g. duplicate domain)
422Unprocessable — monthly limit reached for your plan
429Too Many Requests — rate limit exceeded
500Server Error — something went wrong on our side