REST API

Public REST API for managing domains, users, aliases, DKIM, queue, services, stats, and DNS programmatically.

CeyMail exposes a versioned REST API for programmatic access to your mail server. Automate provisioning, integrate with billing systems like WHMCS, or build custom tooling.

Business feature

The REST API is available on the Business plan and above.

Authentication

All API requests require a Bearer token in the Authorization header:

bash
curl https://mc.example.com/api/v1/domains \
  -H "Authorization: Bearer ceymail_your_token_here"

Creating an API key

  1. Go to Settings > API in the dashboard
  2. Click Create Key
  3. Enter a name (e.g., "WHMCS Production")
  4. Set permissions for each resource (None / Read / Read-Write)
  5. Click Create Key — copy the token immediately (it is only shown once)

Scoped permissions

Each API key has granular read/write permissions per resource:

ResourceReadWrite
DomainsList domainsCreate, delete
UsersList usersCreate, update password, delete
AliasesList aliasesCreate, delete
DKIMList keysGenerate, delete
QueueStats, list itemsFlush, delete messages
StatsSystem metrics— (read-only)
ServicesList statusStart, stop, restart
DNSRecords, verify— (read-only)

Write scope includes read. Stats and DNS are read-only resources — they cannot be granted write scope. DNS verify (POST) requires only read scope since it performs lookups, not data mutation.

Key limits

  • Business plan: up to 5 active keys
  • Enterprise plan: up to 25 active keys

Keys can be revoked and have their scopes updated at any time from the dashboard.

Base URL

https://your-dashboard/api/v1/

All endpoints return JSON. Successful responses use HTTP 200 or 201. Errors return a JSON object with an error field.

Request format

  • GET requests use query parameters for pagination, search, and filtering
  • POST and PATCH requests require Content-Type: application/json header (even for requests with no body, such as queue flush)
  • DELETE requests use path parameters for the resource ID (no Content-Type required)

Rate limiting

API requests are rate-limited per key:

  • Business: 120 requests/minute
  • Enterprise: 300 requests/minute

Exceeding the limit returns HTTP 429 with a Retry-After header.

Pagination

List endpoints return paginated results:

json
{
  "domains": [...],
  "totalItems": 42,
  "page": 1,
  "pageSize": 10,
  "totalPages": 5
}

Query parameters:

ParameterDefaultMaxDescription
page1Page number
pageSize10100Results per page
searchSearch term (searches primary field)
sortFieldvariesColumn to sort by
sortDirascasc or desc

Domains

List domains

GET /api/v1/domains
GET /api/v1/domains?search=example&page=1&pageSize=20&sortField=name&sortDir=asc

Response:

json
{
  "domains": [
    { "id": 1, "name": "example.com", "created_at": "2026-03-01T..." }
  ],
  "totalItems": 1,
  "page": 1,
  "pageSize": 10,
  "totalPages": 1
}

Create domain

POST /api/v1/domains
Content-Type: application/json

{ "name": "newdomain.com" }

Response (201):

json
{ "domain": { "id": 2, "name": "newdomain.com", "created_at": "..." } }

Delete domain

DELETE /api/v1/domains/2

Response: { "message": "Domain deleted" }

Returns 409 if the domain has associated users or aliases.

Users

List users

GET /api/v1/users
GET /api/v1/users?domain=example.com&search=john

Response:

json
{
  "users": [
    {
      "id": 1,
      "domain_id": 1,
      "email": "john@example.com",
      "created_at": "2026-03-01T...",
      "domain_name": "example.com"
    }
  ],
  "totalItems": 1,
  "page": 1,
  "pageSize": 10,
  "totalPages": 1
}

Create user

POST /api/v1/users
Content-Type: application/json

{ "email": "john@example.com", "password": "YourSecurePassword" }

The domain in the email must already exist. Passwords must meet complexity requirements (minimum 8 characters, mixed case, numbers, symbols).

Response (201):

json
{
  "user": {
    "id": 2,
    "domain_id": 1,
    "email": "john@example.com",
    "created_at": "...",
    "domain_name": "example.com"
  }
}

Update password

PATCH /api/v1/users/5
Content-Type: application/json

{ "password": "NewSecurePassword" }

Response: { "message": "Password updated" }

Delete user

DELETE /api/v1/users/5

Response: { "message": "User deleted" }

Aliases

List aliases

GET /api/v1/aliases
GET /api/v1/aliases?search=info

Response:

json
{
  "aliases": [
    {
      "id": 1,
      "domain_id": 1,
      "source": "info@example.com",
      "destination": "john@example.com",
      "created_at": "...",
      "domain_name": "example.com"
    }
  ],
  "totalItems": 1,
  "page": 1,
  "pageSize": 10,
  "totalPages": 1
}

Create alias

POST /api/v1/aliases
Content-Type: application/json

{ "source": "info@example.com", "destination": "john@example.com" }

The source domain must already exist.

Response (201):

json
{
  "alias": {
    "id": 1,
    "domain_id": 1,
    "source": "info@example.com",
    "destination": "john@example.com",
    "created_at": "...",
    "domain_name": "example.com"
  }
}

Delete alias

DELETE /api/v1/aliases/3

Response: { "message": "Alias deleted" }

DKIM

List DKIM keys

GET /api/v1/dkim

Response:

json
{
  "dkim": [
    {
      "domain": "example.com",
      "selector": "mail",
      "hasKey": true,
      "dnsRecord": "mail._domainkey IN TXT \"v=DKIM1; k=rsa; p=MIIBIj...\""
    }
  ]
}

Generate DKIM key

POST /api/v1/dkim
Content-Type: application/json

{ "domain": "example.com" }

Response (201):

json
{ "message": "DKIM key generated", "domain": "example.com", "selector": "mail" }

Delete DKIM key

DELETE /api/v1/dkim/example.com

Response: { "message": "DKIM key deleted" }

Queue

Queue statistics

GET /api/v1/queue

Response:

json
{ "queue": { "active": 0, "deferred": 2, "bounce": 0, "hold": 0, "total": 2 } }

List queue items

GET /api/v1/queue/items

Response:

json
{
  "items": [
    {
      "id": "A1B2C3D4E5",
      "sender": "user@example.com",
      "recipients": ["recipient@other.com"],
      "size": 1234,
      "arrivalTime": "2026-03-22T...",
      "queueName": "deferred",
      "reason": "Connection timed out"
    }
  ],
  "total": 1
}

Flush queue

Force delivery retry for all deferred messages:

POST /api/v1/queue/flush
Content-Type: application/json

Response: { "message": "Queue flushed" }

Delete message

DELETE /api/v1/queue/A1B2C3D4E5

Queue IDs are hex strings (e.g., A1B2C3D4E5).

Response: { "message": "Message deleted" }

Stats

System metrics

GET /api/v1/stats

Response:

json
{
  "cpu_percent": 12.5,
  "memory_used_bytes": 1073741824,
  "memory_total_bytes": 4294967296,
  "disk_used_bytes": 21474836480,
  "disk_total_bytes": 53687091200,
  "mail_queue_size": 2,
  "services_healthy": 7,
  "services_total": 7
}

Services

List services

GET /api/v1/services

Response:

json
{
  "services": [
    {
      "name": "postfix",
      "status": "running",
      "uptime_seconds": 86400,
      "memory_bytes": 52428800,
      "pid": 1234
    }
  ]
}

Control a service

POST /api/v1/services/postfix/restart
Content-Type: application/json

Valid actions: start, stop, restart.

Response:

json
{
  "message": "Service restart completed",
  "service": { "name": "postfix", "status": "running", "uptime_seconds": 5, "memory_bytes": 52428800, "pid": 5678 }
}

AI filter

The ceymail-ai-filter service cannot be controlled via the API. Use the AI Screening settings page instead.

DNS

List DNS records

GET /api/v1/dns/example.com

The domain must exist in your configured domains.

Response:

json
{
  "domain": "example.com",
  "records": [
    {
      "type": "MX",
      "name": "example.com",
      "expected": null,
      "current": "mail.example.com",
      "verified": true
    },
    {
      "type": "SPF",
      "name": "example.com",
      "expected": null,
      "current": "v=spf1 ...",
      "verified": true
    },
    {
      "type": "DKIM",
      "name": "mail._domainkey.example.com",
      "expected": null,
      "current": "v=DKIM1; ...",
      "verified": true
    },
    {
      "type": "DMARC",
      "name": "_dmarc.example.com",
      "expected": null,
      "current": "v=DMARC1; ...",
      "verified": true
    },
    {
      "type": "A",
      "name": "example.com",
      "expected": null,
      "current": "1.2.3.4",
      "verified": true
    }
  ],
  "score": 100
}

Verify DNS records

Force a fresh DNS lookup:

POST /api/v1/dns/example.com/verify
Content-Type: application/json

Returns the same response format as the GET endpoint with fresh lookup results.

Errors

All errors return a consistent format:

json
{ "error": "Domain already exists" }
StatusMeaning
200Success (read or action)
201Created
400Invalid input
401Missing, invalid, or insufficient token
404Resource not found
405Method not allowed
409Conflict (duplicate or FK constraint)
415Missing Content-Type on POST/PATCH
429Rate limit exceeded
500Server error

Quick start

bash
# Set your token
TOKEN='your_api_key_here'
BASE='https://mc.example.com/api/v1'

# List domains
curl -s "$BASE/domains" -H "Authorization: Bearer $TOKEN"

# Create a domain
curl -s -X POST "$BASE/domains" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"name":"newdomain.com"}'

# Create users (use strong, unique passwords)
for u in 'john' 'jane' 'admin'; do
  curl -s -X POST "$BASE/users" \
    -H "Authorization: Bearer $TOKEN" \
    -H "Content-Type: application/json" \
    -d '{"email":"'"$u"'@newdomain.com","password":"'"$(openssl rand -base64 16)"'"}'
done

# Check DNS records
curl -s "$BASE/dns/newdomain.com" -H "Authorization: Bearer $TOKEN"

# Check server stats
curl -s "$BASE/stats" -H "Authorization: Bearer $TOKEN"