Outbound Phone Dial
A component to call a phone number using a dedicated AI assistant, crafted with React and Tailwind CSS.
Preview
Configuration
To make functional, drop this serverless function into your app. This function will use default Vapi endpoint to make phone call.
// app/api/vapi/make-call/route.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
import axios from 'axios';
export async function POST(request: NextRequest) {
const API_KEY = process.env.VAPI_API_KEY;
if (!API_KEY) {
return NextResponse.json({ message: 'API key not found' }, { status: 500 });
}
const { phoneNumberId, assistantId, customerNumber } = await request.json();
if (!phoneNumberId || !assistantId || !customerNumber) {
return NextResponse.json({ message: 'Missing required fields' }, { status: 400 });
}
const headers = {
Authorization: `Bearer ${API_KEY}`,
'Content-Type': 'application/json',
};
const data = {
phoneNumberId: phoneNumberId,
assistantId: assistantId,
customer: {
number: customerNumber,
},
};
try {
const response = await axios.post('https://api.vapi.ai/call/phone', data, { headers });
if (response.status === 201) {
return NextResponse.json({ message: 'Call created successfully', data: response.data });
} else {
return NextResponse.json({ message: 'Failed to create call', error: response.data }, { status: response.status });
}
} catch (error) {
return NextResponse.json({ message: 'Internal Server Error', error: JSON.stringify(error, null, 2) }, { status: 500 });
}
}
Component
Copy and paste the following code into your component, example outbound-dial.tsx.
"use client";
import { CheckIcon, ChevronsUpDown } from "lucide-react";
import React, { useState } from "react";
import PhoneInput, { Value as E164Number, getCountryCallingCode, Country } from "react-phone-number-input";
import flags from "react-phone-number-input/flags";
import { Button } from "@/components/ui/button";
import { Input, InputProps } from "@/components/ui/input";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import { Toaster, toast } from 'sonner';
import { cn } from "@/lib/utils";
import { ScrollArea } from "@/components/ui/scroll-area";
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
} from "@/components/ui/command";
const phoneNumberId = process.env.NEXT_PUBLIC_VAPI_PHONE_ID;
const assistantId = process.env.NEXT_PUBLIC_VAPI_ASSISTANT_ID;
type PhoneInputProps = Omit<
React.InputHTMLAttributes<HTMLInputElement>,
"onChange" | "value"
> &
Omit<React.ComponentProps<typeof PhoneInput>, "onChange"> & {
onChange?: (value: E164Number | undefined) => void;
};
const CustomPhoneInput: React.ForwardRefExoticComponent<PhoneInputProps> =
React.forwardRef<React.ElementRef<typeof PhoneInput>, PhoneInputProps>(
({ className, onChange, ...props }, ref) => {
return (
<PhoneInput
ref={ref}
className={cn("flex", className)}
flagComponent={FlagComponent}
countrySelectComponent={CountrySelect}
inputComponent={InputComponent}
onChange={(value) => onChange?.(value)}
{...props}
/>
);
},
);
CustomPhoneInput.displayName = "CustomPhoneInput";
const InputComponent = React.forwardRef<HTMLInputElement, InputProps>(
({ className, ...props }, ref) => (
<Input
className={cn("rounded-e-lg rounded-s-none", className)}
{...props}
ref={ref}
/>
),
);
InputComponent.displayName = "InputComponent";
type CountrySelectOption = { label: string; value: Country };
type CountrySelectProps = {
disabled?: boolean;
value: Country;
onChange: (value: Country) => void;
options: CountrySelectOption[];
};
const CountrySelect = ({
disabled,
value,
onChange,
options,
}: CountrySelectProps) => {
const handleSelect = React.useCallback(
(country: Country) => {
onChange(country);
},
[onChange],
);
return (
<Popover>
<PopoverTrigger asChild>
<Button
type="button"
variant={"outline"}
className={cn("flex gap-1 rounded-e-none rounded-s-lg px-3")}
disabled={disabled}
>
<FlagComponent country={value} countryName={value} />
<ChevronsUpDown
className={cn(
"-mr-2 h-4 w-4 opacity-50",
disabled ? "hidden" : "opacity-100",
)}
/>
</Button>
</PopoverTrigger>
<PopoverContent className="w-[300px] p-0">
<Command>
<CommandList>
<ScrollArea className="h-72">
<CommandInput placeholder="Search country..." />
<CommandEmpty>No country found.</CommandEmpty>
<CommandGroup>
{options
.filter((x) => x.value)
.map((option) => (
<CommandItem
className="gap-2"
key={option.value}
onSelect={() => handleSelect(option.value)}
>
<FlagComponent
country={option.value}
countryName={option.label}
/>
<span className="flex-1 text-sm">{option.label}</span>
{option.value && (
<span className="text-foreground/50 text-sm">
{`+${getCountryCallingCode(option.value)}`}
</span>
)}
<CheckIcon
className={cn(
"ml-auto h-4 w-4",
option.value === value ? "opacity-100" : "opacity-0",
)}
/>
</CommandItem>
))}
</CommandGroup>
</ScrollArea>
</CommandList>
</Command>
</PopoverContent>
</Popover>
);
};
const FlagComponent = ({ country, countryName }: { country: Country; countryName: string }) => {
const Flag = flags[country];
return (
<span className="bg-foreground/20 flex h-4 w-6 overflow-hidden rounded-sm">
{Flag && <Flag title={countryName} />}
</span>
);
};
FlagComponent.displayName = "FlagComponent";
export default function PhoneInputForm() {
const [phoneNumber, setPhoneNumber] = useState<E164Number | undefined>(undefined);
const [error, setError] = useState("");
const handleSubmit = (e: { preventDefault: () => void; }) => {
e.preventDefault();
if (phoneNumber && validatePhoneNumber(phoneNumber)) {
setError("");
makeCall(phoneNumber);
toast.success(`Dialing ${phoneNumber}`);
console.log(`Phone number: ${phoneNumber}`);
} else {
setError("Please enter a valid phone number.");
}
};
const validatePhoneNumber = (number: string) => {
return /^[+]*[(]{0,1}[0-9]{1,4}[)]{0,1}[-\s./0-9]*$/g.test(number);
};
const makeCall = async (number: string) => {
console.log("Making phone call");
const response = await fetch('/api/vapi/make-call', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
phoneNumberId: phoneNumberId,
assistantId: assistantId,
customerNumber: number,
}),
});
const result = await response.json();
console.log(result);
};
return (
<>
<Toaster position="bottom-right" />
<form onSubmit={handleSubmit} className="mx-auto px-2 items-center">
<p className="mb-2">Enter your phone number to get called by Vapi Blocks.</p>
<div className="flex items-center space-x-2 mb-2">
<CustomPhoneInput
className="w-full"
value={phoneNumber}
onChange={setPhoneNumber}
defaultCountry="US"
countries={['US', 'CA', 'GB']} //add more countries using reference: https://www.npmjs.com/package/react-phone-number-input
/>
</div>
{error && <p className="text-red-500 text-sm mb-2">{error}</p>}
<Button type="submit" size="sm" className="w-full">
Submit
</Button>
</form>
</>
);
}
Usage
Import the component in your file.
import PhoneInputForm from "@/components/vapi/outbound-dial";
export default function Home() {
return (
<main className="w-full min-h-screen">
<PhoneInputForm />
</main>
);
}