Lumail.ioLumail.io
DocsBlogChangelog

Getting Started

  • Introduction
  • Tutorials
  • API Reference
  • Integrations
  • Features
  • Workflows

Tutorials

  • Create an API Token
  • Build a V0 Capture Page
  • Dynamic Promo Codes with Webhooks

API Reference

  • API Tokens
  • Rate Limits
  • POSTSend Transactional Email
  • POSTEmail Verification API
  • POSTCreate Subscriber
  • GETGet Subscriber
  • PATCHUpdate Subscriber
  • DELETEDelete Subscriber
  • POSTAdd Tags to Subscriber
  • DELETERemove Tags from Subscriber
  • POSTTrack Event
  • GETGet Subscriber Events
  • GETGet All Tags
  • POSTCreate a Tag
  • Send Email in HTML
  • Send Email in Markdown
  • Send Email in Tiptap

Integrations

  • ClickFunnels Integration
  • SystemIO Integration

Features

  • Variables
  • Subscriber Events
  • Revenue Tracking
  • Email Deliverability Score
  • Email Engagement Score
  • Content Deliverability Checker

Workflows

  • Wait Step
  • Email Step
  • Action Step
  • Webhook Step

domains

  • Email Domains
  • Web Domains

Build a V0 Capture Page

Guide to generating a Lumail-branded capture page with V0.dev and wiring it to the Subscribers API.

This guide walks you through building a high-converting capture page for Lumail in less than an hour using V0.dev. You will design the page with V0's AI UI builder, connect the form to Lumail's Subscribers API, and keep your organization token secure.

Why use V0.dev?

  • Rapid iteration – generate production-grade Next.js layouts by describing the outcome you want.
  • Consistency – reuse Lumail branding tokens and typography in the prompt to keep the landing page on-brand.
  • Developer handoff – V0 produces file-based Next.js code you can drop into this repository or a standalone capture micro-site.

Prerequisites

  • A Lumail organization with permission to create API tokens.
  • A V0.dev account connected to GitHub (recommended) or ready to download generated code.
  • Node.js 18+ and pnpm if you plan to run the generated project locally.
  • A fresh API token dedicated to this capture page.

Generate a Lumail API token

  1. Open your Lumail organization dashboard and navigate to Settings → API Tokens (the URL will be /orgs/[your-org]/settings/tokens).
  2. Click Generate Token and create a token named v0-capture-page.
  3. Copy the token immediately—it will only be shown once during creation.
  4. Store it securely as an environment variable (we'll set this up in Step 3).

Security Warning: Never expose your API token in client-side code. Always keep it server-side using environment variables and API routes.

Tokens are validated server-side through tokenRoute in app/api/v1/subscribers/route.ts, which delegates to getCurrentOrgFromBearer to ensure every request is tied to the correct organization and enforces plan limits before creating a subscriber.

Step 1 — Prompt V0 for the layout

  1. Create a new V0 project and select Next.js App Router.

  2. In the prompt, describe your target audience, tone, and conversion goal. Example prompt:

    "Design a minimal capture page for Lumail, an AI-assisted email marketing platform. Hero with headline, subheading, email capture form, three value props with icons, customer logos, and a final CTA. Visual style: clean, trustworthy, gradient accents in Lumail teal (#12A6D1)."

  3. Let V0 generate the initial layout. If needed, iterate with follow-up prompts (e.g., "make hero background a subtle gradient," "add testimonials section").

  4. Save the iteration you like as app/(marketing)/capture/page.tsx. V0 will create the file structure for you.

Step 2 — Add the capture form block

  1. Ask V0 to insert a form component underneath the hero copy:

    "Add a form with an email input, optional first name field, and a submit button labeled Join the Lumail waitlist."

  2. Confirm the generated component uses type="email" validation and has accessible labels.

  3. Rename the form component to CaptureForm to keep the file readable.

  4. Add hidden metadata fields if you need tags or source tracking:

    <input type="hidden" name="tags" value='["v0-capture","waitlist"]' />
    <input type="hidden" name="fields[source]" value="lumail.io/v0" />

The Subscribers API will merge tags and fields with the defaults defined in SubscriberSchema (app/api/v1/subscribers/_schemas/api-subscriber.schema.ts).

Step 3 — Connect the form to Lumail securely

Never expose your API token in client-side code. Always create a backend API route that stores the token server-side.

3.1 Set up environment variables

Create a .env.local file in your project root:

LUMAIL_API_TOKEN=your_token_here

Add .env.local to your .gitignore to prevent committing secrets.

3.2 Create a backend API route

Create app/api/subscribe/route.ts to handle form submissions securely:

import { NextRequest, NextResponse } from "next/server";

export async function POST(request: NextRequest) {
  try {
    // Parse the incoming request
    const body = await request.json();

    // Call Lumail API with your token (server-side only)
    const response = await fetch("https://lumail.io/api/v1/subscribers", {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        Authorization: `Bearer ${process.env.LUMAIL_API_TOKEN}`,
      },
      body: JSON.stringify({
        email: body.email,
        name: body.name,
        tags: body.tags || [],
        fields: body.fields || {},
        replaceTags: false,
      }),
    });

    if (!response.ok) {
      const error = await response.json();
      return NextResponse.json(
        { error: error.message || "Failed to subscribe" },
        { status: response.status },
      );
    }

    const data = await response.json();
    return NextResponse.json(data);
  } catch (error) {
    return NextResponse.json(
      { error: "Unable to subscribe right now." },
      { status: 500 },
    );
  }
}

What this does:

  • Receives form data from your frontend
  • Calls Lumail's API with the token stored in .env.local
  • Returns the response to the frontend
  • Token stays on the server and is never exposed to the browser

3.3 Call the API route from your form

Update your V0-generated form component:

"use client";

import { useState } from "react";

export function CaptureForm() {
  const [email, setEmail] = useState("");
  const [name, setName] = useState("");
  const [loading, setLoading] = useState(false);
  const [message, setMessage] = useState("");

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    setLoading(true);
    setMessage("");

    try {
      const response = await fetch("/api/subscribe", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({
          email,
          name,
          tags: ["v0-capture", "waitlist"],
          fields: { source: "lumail.io/v0" },
        }),
      });

      const data = await response.json();

      if (!response.ok) {
        setMessage(data.error || "Something went wrong");
      } else {
        setMessage("You're in! Check your inbox.");
        setEmail("");
        setName("");
      }
    } catch (error) {
      setMessage("Unable to subscribe right now.");
    } finally {
      setLoading(false);
    }
  };

  return (
    <form onSubmit={handleSubmit} className="flex flex-col gap-4">
      <input
        type="email"
        required
        placeholder="Email"
        value={email}
        onChange={(e) => setEmail(e.target.value)}
        className="rounded border px-4 py-2"
      />
      <input
        type="text"
        placeholder="Name (optional)"
        value={name}
        onChange={(e) => setName(e.target.value)}
        className="rounded border px-4 py-2"
      />
      <button
        type="submit"
        disabled={loading}
        className="rounded bg-blue-600 px-6 py-2 text-white disabled:opacity-50"
      >
        {loading ? "Subscribing..." : "Join the waitlist"}
      </button>
      {message && <p className="text-sm">{message}</p>}
    </form>
  );
}

3.4 Better approach with Zod validation

For production apps, add validation using Zod to match Lumail's API schema:

  1. Install Zod: pnpm add zod

  2. Update app/api/subscribe/route.ts:

import { NextRequest, NextResponse } from "next/server";
import { z } from "zod";

// This schema matches Lumail's backend validation
const SubscriberSchema = z.object({
  email: z.string().email(),
  name: z.string().optional().nullable(),
  phone: z.string().optional().nullable(),
  tags: z.array(z.string()).optional().default([]),
  fields: z.record(z.string().optional().nullable()).optional(),
  replaceTags: z.boolean().optional(),
});

export async function POST(request: NextRequest) {
  try {
    const body = await request.json();

    // Validate the request body
    const validated = SubscriberSchema.safeParse(body);
    if (!validated.success) {
      return NextResponse.json(
        { error: "Invalid form data", details: validated.error.errors },
        { status: 400 },
      );
    }

    const response = await fetch("https://lumail.io/api/v1/subscribers", {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        Authorization: `Bearer ${process.env.LUMAIL_API_TOKEN}`,
      },
      body: JSON.stringify(validated.data),
    });

    if (!response.ok) {
      const error = await response.json();
      return NextResponse.json(
        { error: error.message || "Failed to subscribe" },
        { status: response.status },
      );
    }

    const data = await response.json();
    return NextResponse.json(data);
  } catch (error) {
    return NextResponse.json(
      { error: "Unable to subscribe right now." },
      { status: 500 },
    );
  }
}

3.5 Production-ready approach with up-fetch

For even better type safety and automatic validation, use up-fetch:

  1. Install: pnpm add up-fetch

  2. Create lib/upfetch.ts:

import { up } from "up-fetch";

export const upfetch = up(fetch);
  1. Update app/api/subscribe/route.ts:
import { NextRequest, NextResponse } from "next/server";
import { z } from "zod";
import { upfetch } from "@/lib/upfetch";

const SubscriberSchema = z.object({
  email: z.string().email(),
  name: z.string().optional().nullable(),
  tags: z.array(z.string()).optional().default([]),
  fields: z.record(z.string().optional().nullable()).optional(),
});

export async function POST(request: NextRequest) {
  try {
    const body = await request.json();
    const validated = SubscriberSchema.safeParse(body);

    if (!validated.success) {
      return NextResponse.json({ error: "Invalid form data" }, { status: 400 });
    }

    // upfetch automatically validates response and infers types
    const data = await upfetch("https://lumail.io/api/v1/subscribers", {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        Authorization: `Bearer ${process.env.LUMAIL_API_TOKEN}`,
      },
      body: validated.data,
      schema: z.object({
        success: z.literal(true),
        subscriber: z.object({
          id: z.string(),
          email: z.string().email(),
        }),
      }),
    });

    return NextResponse.json(data);
  } catch (error) {
    return NextResponse.json(
      { error: "Unable to subscribe right now." },
      { status: 500 },
    );
  }
}

Benefits of up-fetch:

  • Automatic JSON parsing and serialization
  • Response validation with Zod schemas
  • Full TypeScript type inference
  • Better error handling

When the request reaches POST /api/v1/subscribers, Lumail's handler automatically infers the organization from the token, applies plan limits, and returns the normalized subscriber record.

Step 4 — Tag submissions for downstream workflows

Leverage tags and fields so the new subscribers drop into the right automations:

  • tags: ["v0-capture", "marketing-site"] to segment the list.
  • fields[source] = window.location.pathname captured server-side to track the exact page.
  • fields[utm_campaign] if you pipe UTM parameters into hidden inputs.

Because the backend replaces tags only if replaceTags: true, you can safely append campaign-specific tags without wiping existing labels.

Step 5 — Deploy and test

Local testing

  1. Make sure your .env.local file contains your LUMAIL_API_TOKEN.
  2. Run the dev server: pnpm dev or npm run dev.
  3. Test the form by submitting a real email address.
  4. Check your Lumail dashboard at /orgs/[your-org]/subscribers to confirm the subscriber was added.
  5. Trigger validation errors (empty email, invalid format) to ensure error handling works.

Deploy to Vercel

  1. Push your V0 project to GitHub.
  2. Import the repository into Vercel.
  3. Add environment variable: In Vercel project settings, go to Settings → Environment Variables.
  4. Add LUMAIL_API_TOKEN with your token value for Production, Preview, and Development environments.
  5. Deploy the project.
  6. Test the live form to ensure it's working in production.

Tip: When you rotate your API token in Lumail, remember to update it in Vercel's environment variables and redeploy.

Operational tips

  • Rotate the token periodically. Lumail tokens are stored hashed in Prisma (apiToken table) and tied to plan limits, so revoking a leaked token immediately blocks unauthorized capture pages.
  • Instrument analytics (PostHog or Vercel Web Analytics) to measure form conversion. V0 generates client components that accept additional tracking hooks.
  • Consider adding a double opt-in flow by redirecting to a /thank-you page that explains what to expect next.

Recap

By combining V0.dev's AI-generated UI with Lumail's secure subscriber endpoint, you can launch bespoke capture experiences quickly while keeping organization tokens guarded on the server.

Implementation approaches summary

  1. Basic (Step 3.2-3.3): Plain fetch API with Next.js API routes

    • Simple and straightforward
    • No extra dependencies
    • Good for prototypes and simple projects
    • Manual validation and error handling
  2. Better (Step 3.4): Add Zod validation

    • Schema validation matches Lumail's API
    • Catches errors before making API calls
    • Better error messages for users
    • Requires: zod
  3. Production-ready (Step 3.5): Zod + up-fetch

    • Full type safety with TypeScript inference
    • Automatic JSON handling
    • Response validation
    • Cleaner error handling
    • Requires: zod, up-fetch

Choose the approach that fits your project:

  • Building a quick MVP? Start with the basic approach (3.2-3.3)
  • Launching to real users? Use Zod validation (3.4)
  • Production app with TypeScript? Use up-fetch (3.5)

Reuse this pattern for campaign-specific landing pages, webinar waitlists, or partner co-marketing forms without rewriting the integration.

Related Documentation

  • API Tokens Reference - Complete token management guide
  • Create API Token Tutorial - Step-by-step token creation
  • Create Subscriber API - API endpoint reference
  • Tags API - Manage subscriber tags
  • Workflows - Trigger workflows from form submissions
  • Web Domains - Host capture pages on custom domains
Create an API TokenDynamic Promo Codes with Webhooks

On This Page

Why use V0.dev?PrerequisitesGenerate a Lumail API tokenStep 1 — Prompt V0 for the layoutStep 2 — Add the capture form blockStep 3 — Connect the form to Lumail securely3.1 Set up environment variables3.2 Create a backend API route3.3 Call the API route from your form3.4 Better approach with Zod validation3.5 Production-ready approach with up-fetchStep 4 — Tag submissions for downstream workflowsStep 5 — Deploy and testLocal testingDeploy to VercelOperational tipsRecapImplementation approaches summaryRelated Documentation

Lumail.io

Create and send e-mail without paying thousands of dollars

Product

BlogDocumentationChangelogDashboard

Company

AboutAccount

Legal

TermsPrivacy

8 The Green STE B, Dover Delaware 19901, United States

© 2025 Codelynx, LLC. All rights reserved.

Sign in