diff --git a/content/docs/documentation/making-your-first-request.mdx b/content/docs/documentation/making-your-first-request.mdx
index c1414da..6d78ab2 100644
--- a/content/docs/documentation/making-your-first-request.mdx
+++ b/content/docs/documentation/making-your-first-request.mdx
@@ -4,11 +4,13 @@ description: A beginner-friendly guide to making your first Whitepages API reque
icon: Play
---
-Ready to test your API key? You can try it right here in your browser, or follow the step-by-step guide using Postman.
+Ready to test your API key? Pick the use case that matches what you're building and try it right here in your browser — or follow the step-by-step guide using Postman.
-## Quick Test
+## Try It Now
-
+Paste your API key once, then run a sample request for the use case you care about. Each section links to a deeper guide if you want to learn more.
+
+
## Using Postman (No Code Required)
diff --git a/src/app/api/test-proxy/route.ts b/src/app/api/test-proxy/[endpoint]/route.ts
similarity index 58%
rename from src/app/api/test-proxy/route.ts
rename to src/app/api/test-proxy/[endpoint]/route.ts
index eeeb47c..a5af7e8 100644
--- a/src/app/api/test-proxy/route.ts
+++ b/src/app/api/test-proxy/[endpoint]/route.ts
@@ -1,11 +1,20 @@
import { NextRequest, NextResponse } from "next/server";
-export async function GET(request: NextRequest) {
+const ALLOWED_ENDPOINTS = new Set(["person", "property"]);
+
+export async function GET(
+ request: NextRequest,
+ { params }: { params: Promise<{ endpoint: string }> },
+) {
+ const { endpoint } = await params;
+ if (!ALLOWED_ENDPOINTS.has(endpoint)) {
+ return NextResponse.json({ error: "Invalid endpoint" }, { status: 404 });
+ }
+
const apiKey = request.headers.get("X-Api-Key");
const { searchParams } = new URL(request.url);
-
const queryString = searchParams.toString();
- const url = `https://api.whitepages.com/v2/person?${queryString}`;
+ const url = `https://api.whitepages.com/v2/${endpoint}?${queryString}`;
try {
const response = await fetch(url, {
diff --git a/src/components/activation/api-key-tester.tsx b/src/components/activation/api-key-tester.tsx
deleted file mode 100644
index 2af214d..0000000
--- a/src/components/activation/api-key-tester.tsx
+++ /dev/null
@@ -1,272 +0,0 @@
-"use client";
-
-import { useState } from "react";
-import { buttonVariants } from "@/components/ui/button";
-import amplitude from "@/lib/amplitude";
-
-interface ApiResponse {
- success: boolean;
- message: string;
- details?: string;
- data?: unknown;
- status?: number;
-}
-
-const MOCK_DATA = [
- {
- id: "P1234567890",
- name: "John Smith",
- age_range: "35-44",
- current_addresses: [
- {
- id: "A9876543210",
- address: "123 Main St, Seattle, WA 98101",
- is_current: true,
- },
- ],
- phones: [
- {
- number: "(206) 555-0198",
- type: "mobile",
- is_primary: true,
- },
- ],
- emails: [
- {
- address: "john.smith@email.com",
- score: 88,
- },
- ],
- },
-];
-
-interface ApiKeyTesterProps {
- mockMode?: boolean;
-}
-
-export function ApiKeyTester({ mockMode = false }: ApiKeyTesterProps) {
- const [apiKey, setApiKey] = useState(mockMode ? "demo-api-key-xxxxx" : "");
- const [isLoading, setIsLoading] = useState(false);
- const [response, setResponse] = useState(
- mockMode
- ? {
- success: true,
- message: "Success! Your API key is working.",
- data: MOCK_DATA,
- status: 200,
- }
- : null,
- );
-
- const testApiKey = async () => {
- if (!apiKey.trim()) {
- setResponse({
- success: false,
- message: "Please enter an API key",
- });
- return;
- }
-
- setIsLoading(true);
- setResponse(null);
-
- try {
- const result = await fetch(
- "/docs/api/test-proxy?name=John%20Smith&city=Seattle&state_code=WA",
- {
- headers: {
- "X-Api-Key": apiKey.trim(),
- },
- },
- );
-
- const data = await result.json().catch(() => null);
-
- if (result.ok) {
- amplitude.track("WPAPIDocsApiKeyTested", {
- success: true,
- status: result.status,
- });
- setResponse({
- success: true,
- message: "Success! Your API key is working.",
- data,
- status: result.status,
- });
- } else if (result.status === 403) {
- amplitude.track("WPAPIDocsApiKeyTested", {
- success: false,
- status: result.status,
- error: "invalid_key",
- });
- setResponse({
- success: false,
- message: "Invalid API key",
- details:
- "The API key you entered is not valid. Please check your email for the correct API key.",
- status: result.status,
- });
- } else if (result.status === 429) {
- amplitude.track("WPAPIDocsApiKeyTested", {
- success: false,
- status: result.status,
- error: "rate_limited",
- });
- setResponse({
- success: false,
- message: "Rate limit exceeded",
- details:
- "Your API key is valid but you've exceeded the rate limit. Wait a moment and try again.",
- status: result.status,
- });
- } else {
- amplitude.track("WPAPIDocsApiKeyTested", {
- success: false,
- status: result.status,
- error: "request_failed",
- });
- setResponse({
- success: false,
- message: `Request failed (${result.status})`,
- details: data?.message || "An unexpected error occurred.",
- data,
- status: result.status,
- });
- }
- } catch {
- amplitude.track("WPAPIDocsApiKeyTested", {
- success: false,
- error: "connection_error",
- });
- setResponse({
- success: false,
- message: "Connection error",
- details:
- "Unable to connect to the API. Please check your internet connection and try again.",
- });
- } finally {
- setIsLoading(false);
- }
- };
-
- const handleKeyDown = (event: React.KeyboardEvent) => {
- if (event.key === "Enter" && !isLoading) {
- testApiKey();
- }
- };
-
- return (
-
-
Try It Now
-
- Enter your API key below to make a test request and see real results.
-
-
-
-
-
- setApiKey(event.target.value)}
- onKeyDown={handleKeyDown}
- placeholder="Paste your API key here"
- className="w-full px-3 py-2 border rounded-md bg-fd-background text-fd-foreground focus:outline-none focus:ring-2 focus:ring-fd-ring"
- disabled={isLoading}
- />
-
-
-
-
- {response && (
-
-
-
- {response.success ? (
-
- ) : (
-
- )}
-
-
-
- {response.message}
-
- {response.details && (
-
- {response.details}
-
- )}
-
-
-
- )}
-
- {response?.data !== undefined && (
-
-
Response Data
-
- {JSON.stringify(response.data, null, 2)}
-
-
- )}
-
-
- );
-}
diff --git a/src/components/activation/onboarding-tester.tsx b/src/components/activation/onboarding-tester.tsx
new file mode 100644
index 0000000..960404d
--- /dev/null
+++ b/src/components/activation/onboarding-tester.tsx
@@ -0,0 +1,447 @@
+"use client";
+
+import { useState } from "react";
+import Link from "next/link";
+import { buttonVariants } from "@/components/ui/button";
+import amplitude from "@/lib/amplitude";
+
+interface ApiResponse {
+ success: boolean;
+ message: string;
+ details?: string;
+ data?: unknown;
+ status?: number;
+}
+
+type Endpoint = "person" | "property";
+
+interface FieldConfig {
+ name: string;
+ label: string;
+ placeholder: string;
+ required: boolean;
+}
+
+interface SectionConfig {
+ id: string;
+ title: string;
+ description: string;
+ endpoint: Endpoint;
+ fields: FieldConfig[];
+ guideHref: string;
+ guideLabel: string;
+}
+
+const SECTIONS: SectionConfig[] = [
+ {
+ id: "person_search",
+ title: "Search for a Person",
+ description:
+ "Find a person by name. Narrow results with city, state, or street.",
+ endpoint: "person",
+ fields: [
+ {
+ name: "name",
+ label: "Full name",
+ placeholder: "John Smith",
+ required: true,
+ },
+ {
+ name: "city",
+ label: "City",
+ placeholder: "Seattle",
+ required: false,
+ },
+ {
+ name: "state_code",
+ label: "State",
+ placeholder: "WA",
+ required: false,
+ },
+ {
+ name: "street",
+ label: "Street",
+ placeholder: "123 Main St",
+ required: false,
+ },
+ ],
+ guideHref: "/documentation/person-search",
+ guideLabel: "Read the Person Search guide",
+ },
+ {
+ id: "reverse_phone",
+ title: "Look Up a Phone Number",
+ description: "Find the person associated with a phone number.",
+ endpoint: "person",
+ fields: [
+ {
+ name: "phone",
+ label: "Phone number",
+ placeholder: "2065550198",
+ required: true,
+ },
+ ],
+ guideHref: "/documentation/person-search/reverse-phone-lookup",
+ guideLabel: "Read the Reverse Phone Lookup guide",
+ },
+ {
+ id: "property_search",
+ title: "Search for a Property",
+ description: "Get ownership and resident data for any address.",
+ endpoint: "property",
+ fields: [
+ {
+ name: "street",
+ label: "Street",
+ placeholder: "1600 Pennsylvania Ave NW",
+ required: true,
+ },
+ {
+ name: "city",
+ label: "City",
+ placeholder: "Washington",
+ required: false,
+ },
+ {
+ name: "state_code",
+ label: "State",
+ placeholder: "DC",
+ required: true,
+ },
+ ],
+ guideHref: "/documentation/property-search",
+ guideLabel: "Read the Property Search guide",
+ },
+];
+
+function buildParams(values: Record): URLSearchParams {
+ const params = new URLSearchParams();
+ for (const [key, value] of Object.entries(values)) {
+ const trimmed = value.trim();
+ if (trimmed) params.set(key, trimmed);
+ }
+ return params;
+}
+
+/** Sniff the result count from the V2 response shape.
+ *
+ * Person Search V2 returns ``{ results: [...], metadata: { result_count } }``;
+ * Property Search V2 returns ``{ result: {...} }`` (singular). Returns
+ * ``undefined`` when the shape doesn't match either, so Amplitude doesn't
+ * record a misleading zero. */
+function countResults(data: unknown): number | undefined {
+ if (Array.isArray(data)) return data.length;
+ if (data && typeof data === "object") {
+ const obj = data as Record;
+ const metadata = obj.metadata;
+ if (metadata && typeof metadata === "object") {
+ const count = (metadata as Record).result_count;
+ if (typeof count === "number") return count;
+ }
+ if (Array.isArray(obj.results)) return obj.results.length;
+ if (obj.result && typeof obj.result === "object") return 1;
+ }
+ return undefined;
+}
+
+function classifyError(status: number | undefined, body: unknown): ApiResponse {
+ if (status === 403) {
+ return {
+ success: false,
+ message: "Invalid API key",
+ details:
+ "The API key you entered is not valid. Please check your email for the correct API key.",
+ status,
+ };
+ }
+ if (status === 429) {
+ return {
+ success: false,
+ message: "Usage limit reached",
+ details:
+ "Your API key is valid, but you've hit a usage limit. This may be a rate limit (try again in a moment) or the overall quota for your key (which resets per billing period).",
+ status,
+ };
+ }
+ if (status === 404) {
+ return {
+ success: false,
+ message: "No results found",
+ details:
+ "Your API key is working, but no records matched your search. Try different search parameters.",
+ status,
+ };
+ }
+ const message =
+ (body as { message?: string } | null)?.message ??
+ "An unexpected error occurred.";
+ return {
+ success: false,
+ message: `Request failed (${status ?? "?"})`,
+ details: message,
+ data: body,
+ status,
+ };
+}
+
+interface SectionTesterProps {
+ config: SectionConfig;
+ apiKey: string;
+}
+
+function SectionTester({ config, apiKey }: SectionTesterProps) {
+ const [values, setValues] = useState>(() =>
+ Object.fromEntries(config.fields.map((field) => [field.name, ""])),
+ );
+ const [isLoading, setIsLoading] = useState(false);
+ const [response, setResponse] = useState(null);
+
+ const missingRequired = config.fields
+ .filter((field) => field.required)
+ .some((field) => values[field.name].trim() === "");
+ const canSubmit = apiKey.trim() !== "" && !missingRequired;
+
+ const handleSubmit = async () => {
+ if (!canSubmit) return;
+
+ setIsLoading(true);
+ setResponse(null);
+
+ const params = buildParams(values);
+ amplitude.track("WPAPIDocsOnboardingTestSent", {
+ section: config.id,
+ params: Object.fromEntries(params),
+ });
+
+ try {
+ const result = await fetch(
+ `/docs/api/test-proxy/${config.endpoint}?${params.toString()}`,
+ { headers: { "X-Api-Key": apiKey.trim() } },
+ );
+ const data = await result.json().catch(() => null);
+ const resultCount = result.ok ? countResults(data) : undefined;
+
+ if (result.ok) {
+ amplitude.track("WPAPIDocsOnboardingTestResult", {
+ section: config.id,
+ success: true,
+ status: result.status,
+ result_count: resultCount,
+ });
+ setResponse({
+ success: true,
+ message: "Success! Your request returned data.",
+ data,
+ status: result.status,
+ });
+ } else {
+ const classified = classifyError(result.status, data);
+ amplitude.track("WPAPIDocsOnboardingTestResult", {
+ section: config.id,
+ success: false,
+ status: result.status,
+ error:
+ result.status === 403
+ ? "invalid_key"
+ : result.status === 429
+ ? "rate_limited"
+ : result.status === 404
+ ? "no_results"
+ : "request_failed",
+ });
+ setResponse(classified);
+ }
+ } catch {
+ amplitude.track("WPAPIDocsOnboardingTestResult", {
+ section: config.id,
+ success: false,
+ error: "connection_error",
+ });
+ setResponse({
+ success: false,
+ message: "Connection error",
+ details:
+ "Unable to connect to the API. Please check your internet connection and try again.",
+ });
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ const handleKeyDown = (event: React.KeyboardEvent) => {
+ if (event.key === "Enter" && canSubmit && !isLoading) {
+ handleSubmit();
+ }
+ };
+
+ const handleGuideClick = () => {
+ amplitude.track("WPAPIDocsOnboardingGuideClick", {
+ section: config.id,
+ });
+ };
+
+ return (
+
+ {config.title}
+
+ {config.description}
+
+
+
+ {config.fields.map((field) => (
+
+
+
+ setValues({ ...values, [field.name]: event.target.value })
+ }
+ onKeyDown={handleKeyDown}
+ placeholder={field.placeholder}
+ required={field.required}
+ className="w-full px-3 py-2 border rounded-md bg-fd-background text-fd-foreground focus:outline-none focus:ring-2 focus:ring-fd-ring"
+ disabled={isLoading}
+ />
+
+ ))}
+
+
+
+
+ {config.guideLabel} →
+
+
+
+ {response && (
+
+
+
+ {response.success ? (
+
+ ) : (
+
+ )}
+
+
+
+ {response.message}
+
+ {response.details && (
+
+ {response.details}
+
+ )}
+
+
+
+ )}
+
+ {response?.data !== undefined && (
+
+
Response Data
+
+ {JSON.stringify(response.data, null, 2)}
+
+
+ )}
+
+
+ );
+}
+
+export function OnboardingTester() {
+ const [apiKey, setApiKey] = useState("");
+
+ return (
+
+
+
+
setApiKey(event.target.value)}
+ placeholder="Paste your API key here"
+ className="w-full px-3 py-2 border rounded-md bg-fd-background text-fd-foreground focus:outline-none focus:ring-2 focus:ring-fd-ring"
+ />
+
+ Your API key is shared across all use cases below.
+
+
+
+ {SECTIONS.map((config) => (
+
+ ))}
+
+ );
+}
diff --git a/src/components/webhooks/walkthrough.tsx b/src/components/webhooks/walkthrough.tsx
index 63bd8cb..a5013ea 100644
--- a/src/components/webhooks/walkthrough.tsx
+++ b/src/components/webhooks/walkthrough.tsx
@@ -571,19 +571,19 @@ export function WebhookWalkthrough() {
Create Webhook guide
Event payload reference
Event types
diff --git a/src/mdx-components.tsx b/src/mdx-components.tsx
index 75b0373..fc6487e 100644
--- a/src/mdx-components.tsx
+++ b/src/mdx-components.tsx
@@ -1,7 +1,7 @@
import defaultMdxComponents from "fumadocs-ui/mdx";
import { Step, Steps } from "fumadocs-ui/components/steps";
import { APIPage } from "@/components/openapi/api-page";
-import { ApiKeyTester } from "@/components/activation/api-key-tester";
+import { OnboardingTester } from "@/components/activation/onboarding-tester";
import { RegionSearch } from "@/components/regions/region-search";
import { WebhookWalkthrough } from "@/components/webhooks/walkthrough";
import type { MDXComponents } from "mdx/types";
@@ -12,7 +12,7 @@ export function getMDXComponents(components?: MDXComponents): MDXComponents {
Steps,
Step,
APIPage,
- ApiKeyTester,
+ OnboardingTester,
RegionSearch,
WebhookWalkthrough,
...components,