EmailWalrus API
Plug EmailWalrus into your own dashboard (for example the MarketView admin): mailboxes, sending stats per client, campaigns, replies and live events.
- Base URL:
https://emailwalrus.com - Auth: every request sends
Authorization: Bearer <API key>. Get or regenerate the key at emailwalrus.com → Settings → API key. - Format: JSON in, JSON out. Errors return
{"error": "…"}with a 4xx status. - Limit: 600 requests per minute per key (HTTP 429 when exceeded).
- Keep the key on your server. Call the API from your backend, never from browser JavaScript, or anyone viewing the page could take the key.
Quick start
curl -H "Authorization: Bearer $EMAILWALRUS_KEY" https://emailwalrus.com/v1/stats?days=7
Clients (one EmailWalrus account, many of your clients)
Tag each domain with the client it serves. Its mailboxes, stats and replies then filter by that client everywhere with ?client=.
| Method | Path | What it does |
|---|---|---|
| PATCH | /v1/domains/{id} |
{"client": "Acme Roofing"} tags a domain (send null to clear) |
| GET | /v1/clients |
Every client label with its domain and mailbox counts |
Stats
GET /v1/stats?days=7&client=Acme%20Roofing (days 1–90; client optional)
{
"client": "Acme Roofing", "days": 7, "from": "2026-10-27", "to": "2026-11-02",
"mailboxes": 10, "mailboxes_warming": 4,
"sending_capacity_today": 182, "sending_capacity_full": 300,
"sent": 940, "sent_by_day": [{"day": "2026-10-27", "sent": 120}, "…"],
"bounces": 9, "bounce_rate": 0.0096,
"complaints": 0, "complaint_rate": 0.0,
"replies": 21, "reply_rate": 0.0223, "positive_replies": 6,
"replies_by_category": {"interested": 6, "not_interested": 9, "other": 6},
"heatwave_listed_domains": 0
}
Mailboxes and domains
| Method | Path | What it does |
|---|---|---|
| GET | /v1/me |
Account, mailbox count, monthly cost, sending health |
| GET | /v1/domains?client= |
Domains with status, Heatwave check, renewal date, client |
| POST | /v1/domains |
{"name": "getacme.com", "mailboxes": 3} add a domain you own |
| GET | /v1/domains/{id}/dns |
The DNS records to publish |
| POST | /v1/domains/{id}/verify |
Check DNS; creates the requested mailboxes once it passes |
| POST | /v1/domains/{id}/mailboxes |
{"count": 2} or {"names": ["John Smith"]} |
| GET | /v1/mailboxes?client= |
Every mailbox with today's sends, today's limit and its client |
| POST | /v1/mailboxes/{id}/replace |
Free swap of a burned mailbox |
| DELETE | /v1/mailboxes/{id} |
Delete (billing drops automatically) |
| GET | /v1/mailboxes/export.csv |
SMTP/IMAP logins for every mailbox |
| POST | /v1/orders |
{"brand": "acme", "domains": 4, "names": ["John Smith"]} buy domains + mailboxes |
Campaigns
| Method | Path | What it does |
|---|---|---|
| GET | /v1/campaigns |
All campaigns with stats (leads, sent, replies, reply rate, A/B variants) |
| GET | /v1/campaigns/{id} |
One campaign with its steps and stats |
| POST | /v1/campaigns/{id}/leads |
{"leads": [{"email": "…", "first_name": "…", "company": "…"}]} up to 5,000 per call; skips opt-outs and duplicates |
| POST | /v1/campaigns/{id}/status |
{"status": "active"} or {"status": "paused"} |
Replies (the shared Replies inbox)
| Method | Path | What it does |
|---|---|---|
| GET | /v1/replies?category=&campaign_id=&client=&limit=100 |
Newest replies first. Categories: interested, not_interested, out_of_office, unsubscribe, other |
| GET | /v1/replies/{id} |
The whole conversation (your emails and their replies) and the lead |
| POST | /v1/replies/{id}/reply |
{"body": "…"} answers from the same mailbox, in the same thread |
| POST | /v1/leads/{id}/status |
{"status": "interested" \| "meeting" \| "not_interested" \| "unsubscribed" \| …} |
Webhook (live events)
| Method | Path | What it does |
|---|---|---|
| PUT | /v1/webhook |
{"url": "https://admin.marketviewagency.com/hooks/emailwalrus"} returns a secret once |
| GET | /v1/webhook |
Current URL and event types |
| DELETE | /v1/webhook |
Stop sending events |
Events: reply_received and lead_status_changed, posted as JSON:
{"event": "reply_received", "sent_at": 1793650000, "reply_id": "3f2a…", "mailbox": "john@getacme.com",
"from": "jane@prospect.com", "subject": "Re: quick question", "category": "interested", "lead_email": "jane@prospect.com"}
{"event": "lead_status_changed", "sent_at": 1793650100, "lead_email": "jane@prospect.com", "status": "meeting"}
Every event carries X-EmailWalrus-Timestamp and X-EmailWalrus-Signature. The signature is the hex HMAC-SHA256 of "<timestamp>.<raw body>" with your secret. Reject events whose signature doesn't match or whose timestamp is more than 5 minutes old.
Node (Express)
const crypto = require("crypto");
app.post("/hooks/emailwalrus", express.raw({ type: "application/json" }), (req, res) => {
const ts = req.get("X-EmailWalrus-Timestamp");
const expected = crypto.createHmac("sha256", process.env.EMAILWALRUS_WEBHOOK_SECRET)
.update(`${ts}.`).update(req.body).digest("hex");
const ok = crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(req.get("X-EmailWalrus-Signature") || ""))
&& Math.abs(Date.now() / 1000 - Number(ts)) < 300;
if (!ok) return res.sendStatus(401);
const event = JSON.parse(req.body);
// e.g. mark the lead hot in your CRM when event.event === "reply_received" && event.category === "interested"
res.sendStatus(200);
});
PHP
$body = file_get_contents('php://input');
$ts = $_SERVER['HTTP_X_EMAILWALRUS_TIMESTAMP'] ?? '';
$expected = hash_hmac('sha256', $ts . '.' . $body, getenv('EMAILWALRUS_WEBHOOK_SECRET'));
if (!hash_equals($expected, $_SERVER['HTTP_X_EMAILWALRUS_SIGNATURE'] ?? '') || abs(time() - (int)$ts) > 300) {
http_response_code(401); exit;
}
$event = json_decode($body, true);
Dashboard example (server-side Node)
const EW = "https://emailwalrus.com";
const headers = { Authorization: `Bearer ${process.env.EMAILWALRUS_KEY}` };
const get = (path) => fetch(EW + path, { headers }).then((r) => r.json());
// one card per client on your dashboard
const { clients } = await get("/v1/clients");
for (const c of clients) {
const s = await get(`/v1/stats?days=30&client=${encodeURIComponent(c.client)}`);
console.log(c.client, s.sent, "sent,", s.replies, "replies,", (s.bounce_rate * 100).toFixed(1) + "% bounces");
}