From bee88fdd8eb128cc3dc836eec69b12ad73b2d1af Mon Sep 17 00:00:00 2001 From: Leif Hagen Date: Wed, 10 Jun 2026 16:56:15 -0400 Subject: [PATCH 1/2] feat: sectional onboarding tester for first-request page MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replaces the one-size-fits-all on making-your-first-request.mdx with a three-section matching the three primary API use cases: person search, reverse phone lookup, and property search. The legacy tester only made one boilerplate request (`name=John Smith&city=Seattle&state_code=WA`), which gave a trial user who just received their key no visibility into how the API performs against their own use case. Behavior: - Shared API key field at the top, then three stacked sections. Each section has the form fields its use case actually needs (e.g. phone section asks for a phone number; property section asks for street and state), a Send button, an inline response panel, and a Link to the deeper guide for that use case. - The proxy at /docs/api/test-proxy is now a dynamic route under /[endpoint] with an allowlist (`person`, `property`), so the property section can hit /v2/property while the person/phone sections continue to hit /v2/person. - 4xx responses get human-readable classification rather than a generic failure — 403 is "invalid key", 429 is "rate limit", 404 is "no results found, your key is working". Analytics: - Per-section events on Send (with the outgoing query params), on response (with success/status/result_count), and on guide-link click. The result_count signal sniffs the v2 response shape so we can tell whether a section is producing empty results. - The legacy single-shot test event is replaced by these section-scoped events. Removed: - src/components/activation/api-key-tester.tsx - src/app/api/test-proxy/route.ts (replaced by the dynamic route) --- .../making-your-first-request.mdx | 8 +- .../api/test-proxy/{ => [endpoint]}/route.ts | 15 +- src/components/activation/api-key-tester.tsx | 272 ----------- .../activation/onboarding-tester.tsx | 447 ++++++++++++++++++ src/mdx-components.tsx | 4 +- 5 files changed, 466 insertions(+), 280 deletions(-) rename src/app/api/test-proxy/{ => [endpoint]}/route.ts (58%) delete mode 100644 src/components/activation/api-key-tester.tsx create mode 100644 src/components/activation/onboarding-tester.tsx 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/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, From f55069684f32dfe684d3060e4599acbe0fcff58b Mon Sep 17 00:00:00 2001 From: Leif Hagen Date: Wed, 10 Jun 2026 16:56:23 -0400 Subject: [PATCH 2/2] fix: drop /docs prefix from walkthrough completion links MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The three Link hrefs on the webhook walkthrough completion screen hard-coded the basePath (`/docs/documentation/...`), but next/link's Link auto-applies the `basePath: "/docs"` from next.config.mjs — so the rendered URLs were doubly-prefixed (`/docs/docs/documentation/...`) and 404'd. Surfaced while reviewing the same pattern in the new onboarding tester. Drop the `/docs` prefix to match the convention used in MDX content and in changelog-banner.tsx — bare paths that Link prefixes automatically. Reachable via /docs/documentation/webhooks/quickstart after completing the walkthrough flow. --- src/components/webhooks/walkthrough.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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