# Get a text when a new lead comes in

Add a phone notification to your contact form. Keep the full inquiry in your inbox or CRM and get a short preview on your phone.

This recipe uses Next.js App Router with React 19 and Next.js 15.1 or newer. The interactive preview at https://www.selfping.com/recipes/new-lead-alert is simulated and never sends a text.

## 1. Get your key

Sign in with your own phone number at https://www.selfping.com/login, then copy or create your key at https://www.selfping.com/dashboard. It can only text your phone, not the visitor. Free accounts get 10 texts a month. Every send uses one credit.

## 2. Keep the key on your server

Add this to .env.local in your project root, replace the placeholder, and restart your dev server. Add the same variable to your hosting environment. Never prefix it with NEXT_PUBLIC_ or commit the key.

```dotenv
SELFPING_API_KEY=sp_your_api_key
```

## 3. Add the notification

Create lib/notify-lead.ts:

```ts
import { after } from "next/server";

type Lead = { name: string; email: string; message: string };

export function notifyLead(lead: Lead) {
  after(async () => {
    try {
      const message = Array.from(
        `New lead: ${lead.name} (${lead.email}). ${lead.message}`
          .replace(/\s+/g, " "),
      ).slice(0, 320).join("");

      const response = await fetch("https://www.selfping.com/api/sms", {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
          Authorization: `Bearer ${process.env.SELFPING_API_KEY}`,
        },
        body: JSON.stringify({ message }),
        signal: AbortSignal.timeout(10000),
      });

      if (!response.ok) {
        const body = await response.json().catch(() => null);
        console.error("Lead alert failed:", response.status, body?.error?.code);
      }
    } catch {
      console.error("Lead alert could not be sent.");
    }
  });
}
```

In your existing server-side form handler, call notifyLead only after the inquiry has been saved or accepted by your email service. Keep your existing validation and spam protection.

```ts
import { notifyLead } from "@/lib/notify-lead";

notifyLead({ name, email, message });
```

The notification runs after the form response. A failed text does not change the form's success or erase the inquiry. This is a best-effort alert, not a durable queue. The text is capped at 320 characters; keep the full message in your inbox or CRM. A 200 response means accepted for sending, not delivered. Check delivery on https://www.selfping.com/dashboard.

## Starting without a form

This optional example emails the full inquiry through Resend before scheduling the text. Use a verified sending domain and configure your email addresses. Keep lib/notify-lead.ts from step 3.

```bash
npm install resend zod
```

Add these server-side variables to .env.local and your host:

```dotenv
RESEND_API_KEY=re_your_api_key
LEAD_FROM_EMAIL=Website <leads@your-verified-domain.com>
LEAD_TO_EMAIL=you@example.com
```

app/contact/actions.ts:

```ts
"use server";

import { Resend } from "resend";
import { z } from "zod";
import { notifyLead } from "@/lib/notify-lead";

export type FormState = { success: boolean; message: string };

const leadSchema = z.object({
  name: z.string().trim().min(1).max(80),
  email: z.string().trim().email().max(254),
  message: z.string().trim().min(1).max(5000),
});

export async function submitLead(
  _previous: FormState,
  formData: FormData,
): Promise<FormState> {
  if (formData.get("website")) {
    return { success: true, message: "Thanks. We'll be in touch." };
  }

  const parsed = leadSchema.safeParse({
    name: formData.get("name"),
    email: formData.get("email"),
    message: formData.get("message"),
  });

  if (!parsed.success) {
    return { success: false, message: "Please check your form details." };
  }

  const { name, email, message } = parsed.data;
  const apiKey = process.env.RESEND_API_KEY;
  const from = process.env.LEAD_FROM_EMAIL;
  const to = process.env.LEAD_TO_EMAIL;

  if (!apiKey || !from || !to) {
    return { success: false, message: "The form is unavailable. Try later." };
  }

  try {
    const { error } = await new Resend(apiKey).emails.send({
      from,
      to,
      replyTo: email,
      subject: "New website inquiry",
      text: `Name: ${name}\nEmail: ${email}\n\n${message}`,
    });

    if (error) {
      return { success: false, message: "Couldn't submit. Please try again." };
    }
  } catch {
    return { success: false, message: "Couldn't submit. Please try again." };
  }

  notifyLead(parsed.data);
  return { success: true, message: "Thanks. We'll be in touch." };
}
```

app/contact/page.tsx:

```tsx
"use client";

import { useActionState } from "react";
import { submitLead, type FormState } from "./actions";

const initialState: FormState = { success: false, message: "" };

export default function ContactPage() {
  const [state, action, pending] = useActionState(submitLead, initialState);

  return (
    <main>
      <h1>Get in touch</h1>
      <form action={action}>
        <label htmlFor="name">Name</label>
        <input id="name" name="name" required maxLength={80} />

        <label htmlFor="email">Email</label>
        <input id="email" name="email" type="email" required maxLength={254} />

        <label htmlFor="message">What can we help with?</label>
        <textarea id="message" name="message" required maxLength={5000} />

        <div hidden>
          <label htmlFor="website">Leave this empty</label>
          <input id="website" name="website" tabIndex={-1} autoComplete="off" />
        </div>

        <button type="submit" disabled={pending}>
          {pending ? "Submitting..." : "Send inquiry"}
        </button>
        <p role="status">{state.message}</p>
      </form>
    </main>
  );
}
```

## Try your setup

Submit your own form once with sample details. Check the inquiry reached your inbox or CRM and the text arrived on your phone. This sends a real text and uses one credit.

## Troubleshooting

- No text: check SELFPING_API_KEY on your host and the server logs. 401 means check your key. 429 means check the error code for exhausted credits or a rate limit.
- Accepted but missing: check your dashboard for the final delivery status.
- Wrong phone: texts go only to the number that created the key.

References: [Next.js forms](https://nextjs.org/docs/app/guides/forms), [Next.js after](https://nextjs.org/docs/app/api-reference/functions/after), [server environment variables](https://nextjs.org/docs/app/guides/environment-variables), [React useActionState](https://react.dev/reference/react/useActionState), [Resend with Next.js](https://resend.com/docs/send-with-nextjs).
