Get a text when a new lead comes in.
Someone fills out your contact form. Their inquiry stays in your inbox, and a short alert lands on your phone.
From your form to your phone
Interactive preview. No text is sent.
Your website
Let's work together.
Tell us a little about your project.
Edit the sample details, then try it.
Your phone
SelfPing
New lead: Sam Taylor (sam@example.com). Hi! Could you help us build a new website?
The alert goes to you, not your visitor.
Get your SelfPing key
Sign in with your own phone number, then copy your key from the dashboard. If you don't have a key yet, create one there. Every alert goes to your phone, never the person filling out the form.
Get your free keyAlready using SelfPing? Use your existing key.
Keep the key on your server
Add this to .env.local in your project root and replace the placeholder with your key. Restart your dev server after saving.
SELFPING_API_KEY=sp_your_api_keyAdd the same variable to your hosting environment before deploying. Keep it out of Git and never prefix it with NEXT_PUBLIC_.
Add the notification
Create lib/notify-lead.ts. It sends a short preview after the form response, so a slow or failed alert won't hold up the visitor.
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 it after the inquiry has been saved or accepted by your email service. Use the values you already validated.
import { notifyLead } from "@/lib/notify-lead";
notifyLead({ name, email, message });Keep the full inquiry in your inbox or CRM.
The text is a preview, capped at 320 characters. Delivery can fail, so keep your existing form storage and spam protection. Each send uses one credit.
Starting without a form?Copy a complete form and server action.
This example uses Resend to email the full inquiry first, then calls the helper from step 3. Use an existing App Router project with the @/ import alias.
npm install resend zodGet a Resend API key and verify your sending domain. Add these variables alongside your SelfPing key, locally and on your host:
RESEND_API_KEY=re_your_api_key
LEAD_FROM_EMAIL=Website <leads@your-verified-domain.com>
LEAD_TO_EMAIL=you@example.comAdd these two files. The form reports email failures to the visitor; SMS failures are logged separately. Style the form to match your site and keep your normal spam protection.
"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." };
}"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. This sends a real text and uses one credit.
- The full inquiry reaches your inbox or CRM.
- The short notification arrives on your phone.
- Your SelfPing dashboard shows the final delivery status.
A successful API response means the text was accepted for sending. The dashboard tells you whether it was delivered. This helper sends a best-effort alert; it doesn't queue retries.
If the text doesn't arrive
- Check your key and balance
- Check server logs for the response status. A 401 means check your key. For a 429, check the error code: you may have run out of credits or hit a rate limit.
- Check delivery on your dashboard
- An accepted text can still fail later. Open your dashboard to see the final status and any delivery problem.
- Check the phone number
- Your key texts the number you signed in with. There's no recipient field to set, and the visitor's email is just part of the notification.