Building a Custom Email Dashboard with the Cleanbox API
The Cleanbox dashboard gives you a solid overview of your email activity. But what if you want to embed email metrics in your existing admin panel, create a wall-mounted team dashboard, or build custom visualizations that the built-in dashboard does not offer?
The API exposes everything you need. In this guide, we build a lightweight dashboard that pulls data from multiple API endpoints and renders it in a clean, auto-refreshing interface.
The data sources
| Widget | Endpoint | Data |
|---|---|---|
| Usage gauge | GET /v1/team | Weekly used vs limit, alias count, mailbox count |
| Plan info | GET /v1/team/subscription | Plan name, pricing, specifications |
| Quarantine counter | GET /v1/quarantine | Pending review count |
| Recent messages | GET /v1/messages | Latest emails with status and spam score |
| Active aliases | GET /v1/addresses?type=alias | Alias list with labels and status |
| Top contacts | GET /v1/contacts?sort=emailCount | Most active senders |
| Domain status | GET /v1/domains | Verification and active status |
| Relay overview | GET /v1/relay | Protected domains and addresses |
| Cloud usage | GET /v1/cloud | Storage used vs limit |
Backend: API aggregator
To avoid CORS issues and keep the API key server-side, create a thin backend that aggregates multiple API calls:
# dashboard_api.py
from flask import Flask, jsonify
import requests
app = Flask(__name__)
API_BASE = "https://api.cleanbox.app/v1"
API_KEY = "your_api_key_here"
HEADERS = {"Authorization": f"Bearer {API_KEY}"}
def api_get(path, params=None):
r = requests.get(f"{API_BASE}{path}", headers=HEADERS, params=params)
return r.json() if r.ok else {}
@app.route("/api/dashboard")
def dashboard():
"""Aggregate all dashboard data in a single response."""
team = api_get("/team")
subscription = api_get("/team/subscription")
quarantine = api_get("/quarantine", {"per_page": 1})
messages = api_get("/messages", {"per_page": 10})
addresses = api_get("/addresses", {"type": "alias", "per_page": 100})
contacts = api_get("/contacts", {"sort": "emailCount", "dir": "desc", "per_page": 10})
domains = api_get("/domains")
relay = api_get("/relay")
cloud = api_get("/cloud")
return jsonify({
"team": team,
"subscription": subscription,
"quarantine_count": quarantine.get("total_pages", 0) * 50,
"recent_messages": messages.get("data", []),
"aliases": addresses.get("data", []),
"top_contacts": contacts.get("data", []),
"domains": domains.get("data", []),
"relay": relay.get("data", []),
"cloud": cloud.get("storage", {})
})
if __name__ == "__main__":
app.run(port=5000)
This single endpoint aggregates 9 API calls into one response. The frontend makes one request and gets everything it needs.
Frontend: Dashboard widgets
A minimal HTML + vanilla JavaScript dashboard that auto-refreshes:
<!DOCTYPE html>
<html>
<head>
<title>Email Dashboard</title>
<style>
body { font-family: -apple-system, sans-serif; background: #0f1117; color: #e2e8f0; padding: 24px; }
.grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 16px; }
.card { background: #1a1e2e; border-radius: 12px; padding: 20px; }
.card h3 { color: #94a3b8; font-size: 12px; text-transform: uppercase; letter-spacing: 0.05em; margin: 0 0 12px; }
.big-number { font-size: 36px; font-weight: 700; }
.sub { font-size: 13px; color: #64748b; margin-top: 4px; }
.bar { height: 6px; background: #1e293b; border-radius: 3px; margin-top: 8px; }
.bar-fill { height: 100%; background: #3b82f6; border-radius: 3px; }
.list { font-size: 14px; }
.list-item { padding: 8px 0; border-bottom: 1px solid #1e293b; display: flex; justify-content: space-between; }
.badge { font-size: 11px; padding: 2px 8px; border-radius: 4px; }
.badge-delivered { background: #064e3b; color: #34d399; }
.badge-denied { background: #1e1b4b; color: #818cf8; }
.badge-failed { background: #4a1d2e; color: #f87171; }
</style>
</head>
<body>
<h1 style="margin-bottom:24px">Email Dashboard</h1>
<div class="grid" id="dashboard">Loading...</div>
<script>
async function refresh() {
const res = await fetch("/api/dashboard");
const d = await res.json();
const usage = d.team?.usage || {};
const usagePct = usage.weekly_limit > 0
? Math.round((usage.weekly_used / usage.weekly_limit) * 100) : 0;
const limits = d.team?.limits || {};
const cloud = d.cloud || {};
const cloudPct = cloud.limit > 0
? Math.round((cloud.used / cloud.limit) * 100) : 0;
document.getElementById("dashboard").innerHTML = `
<div class="card">
<h3>Weekly Usage</h3>
<div class="big-number">${usagePct}%</div>
<div class="sub">${usage.weekly_used || 0} / ${usage.weekly_limit || 0} emails</div>
<div class="bar"><div class="bar-fill" style="width:${usagePct}%"></div></div>
</div>
<div class="card">
<h3>Resources</h3>
<div class="list">
<div class="list-item"><span>Aliases</span><span>${limits.aliases?.used || 0} / ${limits.aliases?.limit || 0}</span></div>
<div class="list-item"><span>Mailboxes</span><span>${limits.mailboxes?.used || 0} / ${limits.mailboxes?.limit || 0}</span></div>
<div class="list-item"><span>Domains</span><span>${limits.domains?.used || 0} / ${limits.domains?.limit || 0}</span></div>
<div class="list-item"><span>Filters</span><span>${limits.filters?.used || 0} / ${limits.filters?.limit || 0}</span></div>
</div>
</div>
<div class="card">
<h3>Quarantine</h3>
<div class="big-number">${d.quarantine_count}</div>
<div class="sub">messages pending review</div>
</div>
<div class="card">
<h3>Cloud Storage</h3>
<div class="big-number">${cloudPct}%</div>
<div class="sub">${formatBytes(cloud.used || 0)} / ${formatBytes(cloud.limit || 0)}</div>
<div class="bar"><div class="bar-fill" style="width:${cloudPct}%"></div></div>
</div>
<div class="card" style="grid-column: span 2">
<h3>Recent Messages</h3>
<div class="list">
${d.recent_messages.map(m => `
<div class="list-item">
<span>${escapeHtml(m.from_name || m.from_address)} — ${escapeHtml(m.subject || "(no subject)")}</span>
<span class="badge badge-${m.status}">${m.status}</span>
</div>
`).join("")}
</div>
</div>
<div class="card" style="grid-column: span 2">
<h3>Top Contacts</h3>
<div class="list">
${d.top_contacts.map(c => `
<div class="list-item">
<span>${escapeHtml(c.name || c.email)} <span style="color:#64748b">${c.category?.name || ""}</span></span>
<span>${c.email_count} emails</span>
</div>
`).join("")}
</div>
</div>
`;
}
function formatBytes(bytes) {
if (bytes === 0) return "0 B";
const k = 1024, sizes = ["B", "KB", "MB", "GB"];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + " " + sizes[i];
}
function escapeHtml(str) {
const div = document.createElement("div");
div.textContent = str;
return div.innerHTML;
}
// Initial load + auto-refresh every 60 seconds
refresh();
setInterval(refresh, 60000);
</script>
</body>
</html>
What each widget shows
- Weekly Usage: Circular gauge with percentage and raw numbers from
/v1/team - Resources: Alias, mailbox, domain, and filter counts vs. plan limits
- Quarantine: Number of messages pending review — a growing number means you need to check quarantine
- Cloud Storage: Used vs. available storage from
/v1/cloud - Recent Messages: Last 10 messages with delivery status badges
- Top Contacts: Most active senders with email count and category
Deployment options
- Local:
python3 dashboard_api.pyand openhttp://localhost:5000 - Internal server: Deploy with gunicorn behind nginx for your team
- Wall display: Open in a kiosk-mode browser on a wall-mounted screen
- Embed: Serve the widgets as an iframe in your existing admin panel
Extending further
- Relay status: Add a widget showing relay domains and their address counts from
/v1/relay - Filter activity: List active filters from
/v1/filterswith toggle buttons usingPATCH /v1/filters/{uuid}/toggle - Quarantine actions: Add accept/reject buttons that call
POST /v1/quarantine/acceptandPOST /v1/quarantine/reject - Historical trends: Store snapshots in a database and render charts showing weekly/monthly patterns
The Cleanbox API returns structured JSON for every resource. Anything you see in the dashboard, you can build yourself — styled your way, embedded where you need it.
For the complete endpoint reference, see the API documentation. For authentication setup, see API keys and developer access.