How We Use PageBolt to Send Our Own Monthly Usage Reports
We built PageBolt to generate PDFs and screenshots via API. Then we used it to send our own customers their monthly usage reports. Here's exactly how we did it — and what we learned.
There's a moment in every developer tool company's life where someone on the team says: "wait, can't we just use our own product for this?"
For us, that moment came when we looked at our customer success workflow. We were manually copying usage numbers into a Google Slides template every month, exporting to PDF, and emailing enterprise customers. The irony wasn't lost on us — we'd built an API that generates PDFs from HTML, and we weren't using it ourselves.
Here's exactly what we built, why it works better than what we had before, and the specific code we shipped.
What we were doing before
Monthly, someone on the team would:
- Pull usage stats from our dashboard
- Open the Google Slides template
- Fill in the numbers manually for each customer
- Export to PDF
- Email it via Gmail
For three customers, fine. At 15+ accounts, it was taking most of a morning.
What we built
A cron job that runs on the 1st of every month:
// scripts/monthly-reports.js
import cron from "node-cron";
cron.schedule("0 7 1 * *", async () => {
const { year, month } = getPreviousMonth();
const customers = await db.customers.findMany({
where: { plan: { in: ["growth", "scale"] }, reportsEnabled: true },
include: { owner: true },
});
console.log(`Generating ${customers.length} reports for ${month}/${year}`);
for (const batch of chunk(customers, 5)) {
await Promise.allSettled(batch.map((c) => generateAndSend(c, month, year)));
}
});
The report generation calls our own API:
async function generateAndSend(customer, month, year) {
const metrics = await getMetrics(customer.id, month, year);
const html = renderReport(customer, metrics);
// We call our own /pdf endpoint
const res = await fetch("https://pagebolt.dev/api/v1/pdf", {
method: "POST",
headers: {
"x-api-key": process.env.PAGEBOLT_API_KEY,
"Content-Type": "application/json",
},
body: JSON.stringify({ html }),
});
const pdf = Buffer.from(await res.arrayBuffer());
await emailReport({ customer, pdf, month, year });
console.log(`✓ ${customer.name} — ${pdf.length} bytes`);
}
What the report contains
Each customer gets:
- Total API requests that month (vs previous month)
- Breakdown by endpoint (
/screenshot,/pdf,/video,/og-image) - Top 3 use cases inferred from their API patterns
- Requests remaining in their plan
- A recommendation if they're consistently hitting 80%+ of their limit
The last one was the idea that took us from "nice-to-have report" to "genuine retention tool." If a customer uses 90% of their plan three months in a row, the report says: "You've used 9,400 of 10,000 requests this month. Consider upgrading to Growth to avoid hitting limits during busy periods." It's a soft upsell that arrives in context, not as a sales email.
The HTML template
We kept it simple — no charts, just clean numbers. Our customers are developers; they read tables.
function renderReport(customer, metrics) {
const monthName = new Date(metrics.year, metrics.month - 1)
.toLocaleDateString("en-US", { month: "long", year: "numeric" });
const usagePct = Math.round((metrics.totalRequests / customer.planLimit) * 100);
const showUpgradeNote = usagePct >= 80;
const endpointRows = metrics.byEndpoint
.map((e) => `<tr><td>${e.endpoint}</td><td style="text-align:right">${e.count.toLocaleString()}</td></tr>`)
.join("");
return `<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<style>
body { font-family: -apple-system, sans-serif; color: #111; max-width: 680px; margin: 0 auto; padding: 40px; }
.header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 32px; padding-bottom: 20px; border-bottom: 2px solid #111; }
.logo { font-size: 20px; font-weight: 800; }
.period { color: #666; font-size: 14px; }
.big-number { font-size: 48px; font-weight: 900; line-height: 1; }
.sub { color: #666; font-size: 13px; margin-top: 4px; }
.change { font-size: 13px; margin-top: 6px; }
.up { color: #16a34a; } .down { color: #dc2626; } .flat { color: #888; }
.usage-bar { background: #f0f0f0; border-radius: 4px; height: 8px; margin: 12px 0 4px; overflow: hidden; }
.usage-fill { height: 100%; border-radius: 4px; background: ${usagePct >= 90 ? "#ef4444" : usagePct >= 80 ? "#f59e0b" : "#22c55e"}; width: ${usagePct}%; }
table { width: 100%; border-collapse: collapse; margin-top: 8px; }
th { text-align: left; border-bottom: 2px solid #111; padding: 6px 0; font-size: 11px; text-transform: uppercase; letter-spacing: 0.5px; }
td { padding: 8px 0; border-bottom: 1px solid #f0f0f0; font-size: 13px; }
h2 { font-size: 13px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.5px; color: #666; margin: 32px 0 8px; }
.upgrade-note { background: #fef3c7; border: 1px solid #f59e0b; border-radius: 6px; padding: 14px 16px; margin-top: 24px; font-size: 13px; }
.footer { margin-top: 40px; padding-top: 16px; border-top: 1px solid #eee; color: #999; font-size: 11px; }
</style>
</head>
<body>
<div class="header">
<div class="logo">PageBolt</div>
<div class="period">Usage Report · ${monthName}</div>
</div>
<p style="color:#666;margin-bottom:24px">Hi ${customer.owner.firstName} — here's how ${customer.name} used PageBolt in ${monthName}.</p>
<div class="big-number">${metrics.totalRequests.toLocaleString()}</div>
<div class="sub">API requests</div>
${metrics.prevRequests ? `<div class="change ${metrics.totalRequests >= metrics.prevRequests ? "up" : "down"}">
${metrics.totalRequests >= metrics.prevRequests ? "↑" : "↓"}
${Math.abs(Math.round(((metrics.totalRequests - metrics.prevRequests) / metrics.prevRequests) * 100))}% vs last month
</div>` : ""}
<h2>Plan usage</h2>
<div class="usage-bar"><div class="usage-fill"></div></div>
<div style="font-size:12px;color:#666">${metrics.totalRequests.toLocaleString()} / ${customer.planLimit.toLocaleString()} requests (${usagePct}%)</div>
<h2>Requests by endpoint</h2>
<table>
<thead><tr><th>Endpoint</th><th style="text-align:right">Requests</th></tr></thead>
<tbody>${endpointRows}</tbody>
</table>
${showUpgradeNote ? `
<div class="upgrade-note">
You've used <strong>${usagePct}%</strong> of your plan this month.
If you hit your limit, requests will return 429 errors.
<a href="https://pagebolt.dev/pricing">Upgrade your plan →</a>
</div>` : ""}
<div class="footer">
Questions? Reply to this email or message us at support@pagebolt.dev<br>
Manage your account at <a href="https://pagebolt.dev/dashboard">pagebolt.dev/dashboard</a>
</div>
</body>
</html>`;
}
What we learned
The upgrade note converts. Three customers upgraded within a week of receiving a report showing 80%+ usage. The context — "you used 9,400 requests this month" — makes the upgrade feel rational rather than salesy.
Simple beats fancy. We tried adding charts. Customers didn't mention them. Clean tables with one big number at the top get read; charts get skimmed.
Early customers love it. Receiving a report from a tool you use signals that the company is paying attention. Two customers mentioned it in their next check-in as a reason they felt the product was "serious."
It caught two billing issues. One customer was generating screenshots in a loop — a bug in their code was burning requests. The report made it visible before they hit their limit and had an outage.
The full thing ships in ~80 lines
Excluding the HTML template, the scheduler + generator + email is about 80 lines. We didn't use a reporting library. We didn't set up a data warehouse. We called our own API, the same one our customers call.
If you're building a developer tool and not sending your customers usage reports, you're leaving retention on the table.
Try it free
100 requests/month, no credit card required. OG images, screenshots, PDFs, and video — one API.
Get API Key — Free