EmailWalrus API

Plug EmailWalrus into your own dashboard (for example the MarketView admin): mailboxes, sending stats per client, campaigns, replies and live events.

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");
}