<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Beta Templates]]></title><description><![CDATA[Beta Templates]]></description><link>https://betatemplates.hashnode.dev</link><generator>RSS for Node</generator><lastBuildDate>Wed, 04 Nov 2026 19:10:54 GMT</lastBuildDate><atom:link href="https://betatemplates.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Free Contractor Estimate & Invoice Generator (No Signup, Works on Your Phone)]]></title><description><![CDATA[Most small contractors I talk to still write estimates in a notes app or a Word file they copy every time. It works until a customer asks for a clean PDF…]]></description><link>https://betatemplates.hashnode.dev/free-contractor-estimate-invoice-generator-no-signup-works-on-your-phone</link><guid isPermaLink="true">https://betatemplates.hashnode.dev/free-contractor-estimate-invoice-generator-no-signup-works-on-your-phone</guid><category><![CDATA[Productivity]]></category><category><![CDATA[Small business]]></category><category><![CDATA[JavaScript]]></category><category><![CDATA[Web Development]]></category><dc:creator><![CDATA[Beta Templates]]></dc:creator><pubDate>Fri, 30 Oct 2026 21:11:47 GMT</pubDate><content:encoded><![CDATA[<p>Most small contractors I talk to still write estimates in a notes app or a Word file they copy every time. It works until a customer asks for a clean PDF, or you forget to add tax, or two jobs end up with the same invoice number.</p>
<p>So I built a free, no-signup estimate and invoice generator that runs entirely in the browser: <a href="https://free-contractor-estimate.vercel.app">Free Contractor Estimate &amp; Invoice Generator</a>.</p>
<h2>What it does</h2>
<ul>
<li><strong>Estimates and invoices</strong> with line items, quantities, unit prices, tax, and a deposit line. Print or save as PDF straight from the browser.</li>
<li><strong>Nothing leaves your device.</strong> There's no account and no server storing your customer data.</li>
<li><strong>A job pricing calculator</strong> (<a href="https://free-contractor-estimate.vercel.app/job-pricing-calculator/">open it here</a>) that takes labor hours, crew rate, materials, overhead, and target margin and gives you a price that actually covers the business, not just the job.</li>
<li><strong>Trade-specific starting templates</strong> with typical line items already filled in:<ul>
<li><a href="https://free-contractor-estimate.vercel.app/roofing-estimate-template/">Roofing estimate template</a></li>
<li><a href="https://free-contractor-estimate.vercel.app/plumbing-estimate-template/">Plumbing estimate template</a></li>
<li><a href="https://free-contractor-estimate.vercel.app/hvac-estimate-template/">HVAC estimate template</a></li>
<li><a href="https://free-contractor-estimate.vercel.app/electrical-estimate-template/">Electrical estimate template</a></li>
<li><a href="https://free-contractor-estimate.vercel.app/painting-estimate-template/">Painting estimate template</a></li>
<li><a href="https://free-contractor-estimate.vercel.app/landscaping-estimate-template/">Landscaping estimate template</a></li>
<li><a href="https://free-contractor-estimate.vercel.app/concrete-estimate-template/">Concrete estimate template</a></li>
<li><a href="https://free-contractor-estimate.vercel.app/remodeling-estimate-template/">Remodeling estimate template</a></li>
<li><a href="https://free-contractor-estimate.vercel.app/cleaning-estimate-template/">Cleaning estimate template</a></li>
<li><a href="https://free-contractor-estimate.vercel.app/handyman-estimate-template/">Handyman estimate template</a></li>
</ul>
</li>
</ul>
<h2>Why margin matters more than markup</h2>
<p>The most common pricing mistake I see is adding 20% markup to materials and labor and calling it profit. A 20% markup is only a 16.7% margin, and once overhead (truck, insurance, phone, software, unpaid estimate time) comes out, it can be close to zero. The calculator works backward from the margin you want, so the number you quote already includes overhead.</p>
<h2>How it's built</h2>
<p>Plain HTML, CSS, and a small vanilla JavaScript file, deployed as a static site on Vercel. Estimates are rendered to a print stylesheet, so "Save as PDF" in any browser produces a clean one-page document. No framework and no backend means it loads instantly on a phone at a job site.</p>
<h2>If you want more than one-off estimates</h2>
<p>The free tool handles a single estimate or invoice. If you want to track every lead, quote, job, and payment in one place, I also made a couple of low-cost trackers:</p>
<ul>
<li><a href="https://whop.com/beta-demo-roofing-template/contractor-job-tracker-spreadsheet/">Contractor Job Tracker spreadsheet</a> for Google Sheets or Excel</li>
<li><a href="https://whop.com/beta-demo-roofing-template/contractor-job-tracker-notion/">Contractor Job Tracker for Notion</a></li>
<li><a href="https://whop.com/beta-demo-roofing-template/ai-prompts-for-contractors/">AI Prompts for Contractors</a>, copy-paste prompts for estimates, follow-ups, reviews, and job descriptions</li>
</ul>
<p>But the estimate generator is free and will stay free. If you run a trade business and something is missing, tell me in the comments and I'll add it.</p>
]]></content:encoded></item><item><title><![CDATA[How I Track Contractor Jobs in Notion, From First Call to Final Payment]]></title><description><![CDATA[Four linked databases for leads, quotes, jobs, and invoices]]></description><link>https://betatemplates.hashnode.dev/how-i-track-contractor-jobs-in-notion-from-first-call-to-final-payment</link><guid isPermaLink="true">https://betatemplates.hashnode.dev/how-i-track-contractor-jobs-in-notion-from-first-call-to-final-payment</guid><category><![CDATA[Small business]]></category><category><![CDATA[templates]]></category><category><![CDATA[notion]]></category><category><![CDATA[Productivity]]></category><dc:creator><![CDATA[Beta Templates]]></dc:creator><pubDate>Fri, 30 Oct 2026 17:07:56 GMT</pubDate><content:encoded><![CDATA[<p>Most small contracting shops I talk to run the business out of three places: a notes app full of leads, a folder of quote PDFs, and an invoicing tool that doesn't know either of the other two exist. Jobs fall through the cracks right between "sent the quote" and "got paid."</p>
<p>I built a Notion workspace that keeps that whole pipeline in one place. Here's how it's structured, so you can copy the idea even if you never buy anything.</p>
<h2>The four linked databases</h2>
<ol>
<li><strong>Leads</strong>: every inquiry, with status (New, Contacted, Site visit, Quoted, Won, Lost), source (Google, referral, Facebook, yard sign), service, estimated value, and a next follow-up date. A board view by status lets you drag leads through the pipeline.</li>
<li><strong>Quotes</strong>: one row per estimate, linked to its lead, with quote date, valid-until date, subtotal, tax rate, total, deposit percentage, and a balance formula (total minus deposit). Status goes Draft, Sent, Accepted, or Declined.</li>
<li><strong>Jobs</strong>: once a quote is accepted it becomes a job, with crew, start and end dates, job address, total, deposit, and balance. There's a status board and a calendar view by start date.</li>
<li><strong>Invoices</strong>: deposit and final invoices linked to the job, with due date, amount paid, paid date, and a "Days overdue" formula on anything not yet paid.</li>
</ol>
<p>Because everything is a relation, you can open one job and trace it back to the quote and the original lead source.</p>
<h2>Tips if you build your own</h2>
<ul>
<li>Keep statuses short and few. Six is plenty; nobody updates a 12-step pipeline from a truck.</li>
<li>Put a "Next follow-up" date on every lead and filter a view to today or earlier. That one view does more than any CRM feature.</li>
<li>Split invoices into a deposit and a final balance. It makes "who still owes me money" a single filtered list.</li>
</ul>
<h2>Want the ready-made version?</h2>
<p>If you'd rather skip the setup, the finished template (all four databases, formulas, and views, with a quick-start guide) is $9: <a href="https://whop.com/beta-demo-roofing-template/contractor-job-tracker-notion/">Contractor Job Tracker for Notion</a>.</p>
<p>Prefer spreadsheets? There's also an Excel/Google Sheets version with the same pipeline for $12: <a href="https://whop.com/beta-demo-roofing-template/contractor-job-tracker-spreadsheet/">Contractor Job Tracker spreadsheet</a>.</p>
]]></content:encoded></item><item><title><![CDATA[Server-side mirroring of Whop pixel events]]></title><description><![CDATA[I added three custom conversion events to a contractor site hosted on Whop Websites: service_viewed, deposit_started and estimate_requested. Every call in…]]></description><link>https://betatemplates.hashnode.dev/server-side-mirroring-of-whop-pixel-events</link><guid isPermaLink="true">https://betatemplates.hashnode.dev/server-side-mirroring-of-whop-pixel-events</guid><category><![CDATA[webdev]]></category><category><![CDATA[JavaScript]]></category><category><![CDATA[api]]></category><category><![CDATA[Tutorial]]></category><dc:creator><![CDATA[Beta Templates]]></dc:creator><pubDate>Fri, 30 Oct 2026 14:00:45 GMT</pubDate><content:encoded><![CDATA[<p>I added three custom conversion events to a contractor site hosted on Whop Websites: <code>service_viewed</code>, <code>deposit_started</code> and <code>estimate_requested</code>. Every call in the browser looked fine, and the pixel endpoint answered <code>200</code>. Then I opened the Events page in the dashboard and it was nearly empty.</p>
<p>Two things were going on:</p>
<ol>
<li><strong>Reporting lag.</strong> Custom events take about 15–25 minutes to show up. Wait before you start debugging.</li>
<li><strong>Intake filtering.</strong> The pixel returns <code>200</code> even when Whop's intake drops the visit, which happens with headless or data-center browsers, ad blockers, or a pixel script that never loaded. Those events never arrive.</li>
</ol>
<p>The second one matters most for demos, QA runs and privacy-minded visitors. The fix is to send every event twice, once from the browser and once from the server, with the <strong>same <code>event_id</code></strong> so Whop counts it only once.</p>
<h2>The client: queue for the pixel, then mirror to your server</h2>
<pre><code class="language-ts">// src/track.ts
export function track(event: RoofingEvent, props: Record&lt;string, unknown&gt; = {}) {
  const eventId = `${event}_${crypto.randomUUID()}`;
  const payload = { event_id: eventId, ...props };
  pixelQueue.push([event, payload]);
  if (!flushPixel()) {
    let tries = 0;
    const id = setInterval(() =&gt; { if (flushPixel() || ++tries &gt; 20) clearInterval(id); }, 250);
  }
  mirror(event, eventId, props);
}
</code></pre>
<p><code>flushPixel()</code> drains the queue once <code>window.whop.track</code> exists. That covers the race where a user clicks before the injected pixel has loaded. <code>mirror()</code> POSTs the same event to <code>/api/track</code> with <code>keepalive: true</code>, so it survives navigation to checkout:</p>
<pre><code class="language-ts">fetch("/api/track", {
  method: "POST", headers: { "content-type": "application/json" },
  body, keepalive: true, credentials: "same-origin",
}).catch(() =&gt; {});
</code></pre>
<p>The anonymous ID comes from Whop's <code>_wuid</code> cookie, so the server copy is tied to the same visitor as the pixel copy.</p>
<h2>The server: allowlist, validate, forward to the Events API</h2>
<pre><code class="language-ts">// worker/index.ts
const TRACKED = new Set(["service_viewed", "deposit_started", "estimate_requested"]);

async function mirrorEvent(req: Request, env: Env) {
  const b: any = await req.json().catch(() =&gt; ({}));
  const name = clean(b.event, 35);
  if (!TRACKED.has(name)) return json({ error: "Unknown event" }, 400);
  const eventId = clean(b.event_id, 100);
  if (!/^[\w-]{8,100}$/.test(eventId)) return json({ error: "event_id required" }, 400);
  const cookieWuid = (req.headers.get("cookie") || "").match(/(?:^|;\s*)_wuid=([^;]*)/)?.[1];
  const body = {
    account_id: accountId(env),
    event_name: name,
    event_id: eventId,                 // same id as the pixel → deduplicated
    event_time: new Date().toISOString(),
    action_source: "website",
    url: clean(b.page_url, 1000) || req.headers.get("referer") || undefined,
    user: (b.anonymous_id || cookieWuid) ? { anonymous_id: b.anonymous_id || cookieWuid } : undefined,
    context: { ip_address: req.headers.get("cf-connecting-ip") || undefined,
               user_agent: req.headers.get("user-agent") || undefined },
  };
  let r = await whopApi(env, "/events", { method: "POST", body: JSON.stringify(body) });
  if (!r.ok &amp;&amp; /context\./.test(String(r.body?.error?.message))) {
    delete (body as any).context;      // hosting proxy IP/UA rejected → retry without it
    r = await whopApi(env, "/events", { method: "POST", body: JSON.stringify(body) });
  }
  return json({ ok: r.ok, event_id: eventId }, r.ok ? 200 : 502);
}
</code></pre>
<p>A few choices worth explaining:</p>
<ul>
<li><strong>An allowlist, not a passthrough.</strong> Without it, anyone could use <code>/api/track</code> to inject arbitrary events into your analytics.</li>
<li><strong>Validate <code>event_id</code>.</strong> Deduplication depends on it, so reject anything missing or malformed.</li>
<li><strong>Retry without <code>context</code>.</strong> Behind the hosting proxy, the IP or user agent is sometimes rejected. Dropping device context is better than dropping the event.</li>
<li><strong>The server key never reaches the browser.</strong> Whop hosting injects it into the worker environment.</li>
</ul>
<h2>Verifying it</h2>
<p>Check the <strong>Websites → Pixel events</strong> panel instead of the general Events page, and give it 20 minutes. Run a headless browser against the site. Before the mirror, its events were missing. After it, they all showed up, each counted once.</p>
<hr />
<p><em>This tracking setup comes with my Whop contractor templates, along with Leads-powered estimate forms, deposits, and card-on-file invoicing. Demos: <a href="https://beta-roofing-demo.whop.site">roofing</a> · <a href="https://beta-hvac-demo.whop.site">HVAC</a>. The <a href="https://whop.com/beta-demo-roofing-template/whop-hvac-site-template/">source is $49, or $199 done-for-you</a>.</em></p>
]]></content:encoded></item><item><title><![CDATA[Charging a card on file after the job with the Whop Invoices API]]></title><description><![CDATA[Service businesses like roofers, HVAC techs and landscapers have a payment problem that most checkout tutorials skip. The customer approves a quote today…]]></description><link>https://betatemplates.hashnode.dev/charging-a-card-on-file-after-the-job-with-the-whop-invoices-api</link><guid isPermaLink="true">https://betatemplates.hashnode.dev/charging-a-card-on-file-after-the-job-with-the-whop-invoices-api</guid><category><![CDATA[webdev]]></category><category><![CDATA[JavaScript]]></category><category><![CDATA[api]]></category><category><![CDATA[Tutorial]]></category><dc:creator><![CDATA[Beta Templates]]></dc:creator><pubDate>Fri, 30 Oct 2026 13:59:53 GMT</pubDate><content:encoded><![CDATA[<p>Service businesses like roofers, HVAC techs and landscapers have a payment problem that most checkout tutorials skip. The customer approves a quote today, but nobody knows the final number until the job is done. Collecting the full amount upfront feels wrong, and chasing a check afterwards is worse.</p>
<p>When I built a roofing contractor site on Whop, I used a two-step flow:</p>
<ol>
<li><strong>At approval:</strong> the homeowner saves a card in a setup-mode checkout and is charged $0.</li>
<li><strong>After the job:</strong> the contractor calls one endpoint, which creates an invoice for the final balance and charges the saved card.</li>
</ol>
<p>Here's the code that does it.</p>
<h2>Step 1: Save a card with a setup-mode checkout</h2>
<p>In the Whop dashboard (or through the API) you create a checkout configuration in setup mode. It collects a payment method without charging it. On the site, that configuration ID goes into the same <code>&lt;Checkout&gt;</code> component you'd use for a normal plan:</p>
<pre><code class="language-tsx">// src/CheckoutModal.tsx
const checkoutProps =
  target.kind === "plan"
    ? { plan: target.plan, ...(promoCode ? { promoCode } : {}) }
    : { checkoutConfiguration: target.checkoutConfiguration };

const onComplete = (p: any) =&gt; {
  if (p?.result === "setup")
    setDone({ label: "Card saved on file — nothing was charged.", id: p.setupIntentId });
  else
    setDone({ label: "Payment received — thank you!", id: p?.receiptId ?? p?.paymentId });
};
</code></pre>
<p><code>onComplete</code> returns a <code>setupIntentId</code>. That's the only thing to keep, and you can look up the reusable payment method from it later.</p>
<h2>Step 2: Resolve the saved payment method</h2>
<p>On the worker, an admin-only route reads the setup intent, which carries the <code>payt_…</code> payment method ID:</p>
<pre><code class="language-ts">// worker/index.ts
async function getSetupIntent(id: string, env: Env) {
  const r = await whopApi(env, `/setup_intents/${encodeURIComponent(id)}`);
  return json(r.body, r.status);
}
</code></pre>
<p>Every <code>/api/admin/*</code> route checks an <code>x-admin-token</code> header against a secret, so customers can't reach it.</p>
<h2>Step 3: Create and issue the final invoice</h2>
<p>This is the core of it. The invoice is a one-off hidden plan with the final amount. If you pass <code>payment_method_id</code>, issuing the invoice charges that card. Without one, the customer gets a payable invoice link instead:</p>
<pre><code class="language-ts">const create = await whopApi(env, "/invoices", {
  method: "POST",
  body: JSON.stringify({
    account_id: accountId(env),
    ...(b.member_id
      ? { member_id: b.member_id }
      : { email_address: clean(b.email, 200), customer_name: clean(b.customer_name, 120) || undefined }),
    ...(b.payment_method_id ? { payment_method_id: b.payment_method_id } : {}),
    ...(b.billing_address ? { billing_address: b.billing_address } : {}),
    due_at: due,
    items: [{
      quantity: 1,
      variant: {
        currency: "usd", billing_type: "one_time", initial_price: amount, visibility: "hidden",
        title: clean(b.description, 30) || "Final balance",
        product_id: LEAD_PRODUCT_ID, force_create_new_plan: true,
      },
    }],
    metadata: { kind: "final_balance", project: clean(b.project, 60) || "roofing" },
  }),
});
const issued = await whopApi(env, `/invoices/${create.body.id}/issue`, { method: "POST", body: "{}" });
</code></pre>
<h2>Gotchas I hit</h2>
<ul>
<li><strong>Variant titles are capped at 30 characters.</strong> Put the long text in <code>description</code> and keep <code>title</code> short.</li>
<li><strong>Tax needs an address.</strong> Whop has to resolve tax before it can issue the invoice, so pass <code>billing_address</code> or issuing will fail.</li>
<li><strong>Use <code>force_create_new_plan: true</code>.</strong> Each job's final balance is different, so each invoice gets its own hidden plan instead of reusing one.</li>
<li><strong>Create first, then issue.</strong> The invoice starts as a draft. The charge only happens on <code>/issue</code>, which lets you review the amount before it bills.</li>
</ul>
<p>Calling it from the contractor's side is a single request:</p>
<pre><code class="language-bash">curl -X POST https://your-site.whop.site/api/admin/final-invoice \
  -H "x-admin-token: $ADMIN_TOKEN" -H "content-type: application/json" \
  -d '{"email":"homeowner@example.com","amount":8420,"description":"Roof replacement – final","payment_method_id":"payt_..."}'
</code></pre>
<p>That's the whole flow: a deposit or $0 card save when the quote is approved, and one call when the crew leaves.</p>
<hr />
<p><em>I packaged this flow, along with Whop Leads estimate forms, embedded checkout and server-side events, into two ready-to-deploy contractor templates. Demos: <a href="https://beta-roofing-demo.whop.site">roofing</a> · <a href="https://beta-hvac-demo.whop.site">HVAC</a>. Each is <a href="https://whop.com/beta-demo-roofing-template/whop-roofing-site-template/">$49 for the source or $199 done-for-you</a>.</em></p>
]]></content:encoded></item><item><title><![CDATA[I built contractor websites that run entirely on Whop (leads, deposits, card-on-file invoices)]]></title><description><![CDATA[Most contractor websites stop at a contact form. The homeowner fills it out, someone calls back days later, and half the leads go cold. I wanted a…]]></description><link>https://betatemplates.hashnode.dev/i-built-contractor-websites-that-run-entirely-on-whop-leads-deposits-card-on-file-invoices</link><guid isPermaLink="true">https://betatemplates.hashnode.dev/i-built-contractor-websites-that-run-entirely-on-whop-leads-deposits-card-on-file-invoices</guid><category><![CDATA[webdev]]></category><category><![CDATA[React]]></category><category><![CDATA[JavaScript]]></category><category><![CDATA[Freelancing]]></category><dc:creator><![CDATA[Beta Templates]]></dc:creator><pubDate>Fri, 30 Oct 2026 04:27:20 GMT</pubDate><content:encoded><![CDATA[<p>Most contractor websites stop at a contact form. The homeowner fills it out, someone calls back days later, and half the leads go cold. I wanted a contractor site that actually books the job and takes the money, so I built one that runs entirely on Whop.</p>
<h2>What it does</h2>
<ul>
<li><strong>Estimate requests become real leads.</strong> The estimate form signs the homeowner in with Whop (OAuth + PKCE) and creates a record through the Whop Leads API, so nothing lives in a forgotten inbox.</li>
<li><strong>Deposits and service fees are paid on the spot.</strong> An embedded Whop checkout (with Apple Pay and Google Pay) handles a fixed-price visit and a $500 project deposit without leaving the page.</li>
<li><strong>Card on file, then the final bill.</strong> A setup-mode checkout saves the customer's card. When the job is done, one call to the Invoices API charges the final balance to that saved card.</li>
<li><strong>Conversion tracking that survives ad blockers.</strong> Three events (service viewed, deposit started, estimate requested) fire through the Whop pixel from the browser and are mirrored through a small server endpoint.</li>
<li><strong>One config file to rebrand.</strong> Business name, services, prices, service area and colors all live in one file.</li>
</ul>
<h2>Stack</h2>
<p>React + Vite, deployed to Whop Websites. No Stripe account, no WordPress plugins, no separate CRM.</p>
<h2>Live demos</h2>
<ul>
<li>Roofing: <a href="https://beta-roofing-demo.whop.site">https://beta-roofing-demo.whop.site</a></li>
<li>HVAC: <a href="https://beta-hvac-demo.whop.site">https://beta-hvac-demo.whop.site</a></li>
</ul>
<h2>Lessons from the Whop APIs</h2>
<ol>
<li>Pixel events show up in the dashboard 15 to 25 minutes late. If your events page looks empty, wait before debugging, or check the Websites pixel events panel.</li>
<li>Mirror conversion events server-side. Browser-only tracking quietly loses a chunk of events.</li>
<li>Setup-mode checkout plus the Invoices API is a clean pattern for any service business that bills after the work is done.</li>
</ol>
<h2>Get the source</h2>
<p>If you build sites for roofers or HVAC companies, the full source is available:</p>
<ul>
<li>Roofing template: <a href="https://whop.com/beta-demo-roofing-template/whop-roofing-site-template/">https://whop.com/beta-demo-roofing-template/whop-roofing-site-template/</a></li>
<li>HVAC template: <a href="https://whop.com/beta-demo-roofing-template/whop-hvac-site-template/">https://whop.com/beta-demo-roofing-template/whop-hvac-site-template/</a></li>
</ul>
<p>Each is $49 for the code, or $199 and I'll set it up and rebrand it for you within 3 business days. Happy to answer questions about the Whop APIs in the comments.</p>
]]></content:encoded></item></channel></rss>