To integrate ADP with V0 by Vercel, generate an HR dashboard UI with V0, create a Next.js API route that calls the ADP Workforce Now API using OAuth 2.0 client credentials, store your ADP credentials in Vercel environment variables, and deploy. Your app can display employee data, payroll summaries, and time/attendance records securely without exposing credentials to the browser.
Build HR and Payroll Dashboards with ADP and V0 by Vercel
ADP Workforce Now powers payroll and HR for hundreds of thousands of companies worldwide, making it the most common payroll data source for custom internal tools. When you build a V0-generated dashboard that connects to ADP, you give HR teams a custom view of their workforce data — filtered, formatted, and combined with other business data in ways ADP's own interface does not support. Common use cases include headcount dashboards, department cost reports, time and attendance summaries, and new hire onboarding trackers.
ADP's API platform uses OAuth 2.0 with client credentials for server-to-server integrations, which is the right approach for Next.js API routes. You register as an ADP partner developer, receive a client ID and client secret, and your API routes exchange those credentials for a bearer token before calling ADP's RESTful endpoints. The token expires after a set period, so your routes should implement token caching to avoid re-authenticating on every request — a simple in-memory cache or a short-lived KV store works well in Vercel's serverless environment.
The ADP API surface covers workers (employee profiles and demographics), payroll (pay statements, deductions, earnings), time management (timecards, schedules, accruals), and talent management (performance reviews, goals). For most V0 dashboard projects, the workers and payroll APIs provide the most business value and have the most straightforward data structures. V0 can generate polished table components, charts, and filter controls that bring ADP's raw JSON data to life in a way the default ADP interface cannot match.
Integration method
ADP integrates with V0-generated Next.js apps through server-side API routes that authenticate with ADP's OAuth 2.0 client credentials flow and call ADP Workforce Now APIs. Your ADP client ID and secret are stored as server-only Vercel environment variables and never reach the browser. The V0-generated dashboard UI fetches HR data through your Next.js routes, which proxy requests to ADP's secure API endpoints for employee data, payroll, and time/attendance information.
Prerequisites
- An ADP account with API access — you must be registered as an ADP partner developer at developers.adp.com to obtain API credentials
- ADP API client ID and client secret from the ADP Developer Portal — these are generated when you create an app registration in the ADP Marketplace
- The ADP API base URL for your environment (sandbox: https://accounts.adp.com for auth, https://api.adp.com for data) — use sandbox for development and testing
- A V0 account at v0.dev and a Vercel account for deploying the Next.js app
- Basic understanding of OAuth 2.0 client credentials flow — your server exchanges client ID and secret for a bearer token used in subsequent API calls
Step-by-step guide
Generate the HR Dashboard UI with V0
Generate the HR Dashboard UI with V0
Open V0 at v0.dev and describe the HR interface you want to build. ADP data is structured and tabular, so V0's table and card components work very well for displaying employee records, payroll data, and attendance metrics. Be specific about the data fields you want to display, the filters and search functionality you need, and the visual style. For an employee directory, describe the table columns, pagination behavior, and filter controls. For a payroll dashboard, describe the KPI cards, chart types, and reporting periods. V0 generates React components with Tailwind CSS and shadcn/ui, so you can ask for specific component patterns like data tables with sorting, filter dropdowns, date range pickers, and metric cards. The key information to give V0 is which API route your component will call (e.g., /api/adp/workers for employee data), what the response shape looks like (an array of worker objects with name, jobTitle, department, etc.), and how you want pagination to work. Include loading skeleton states and empty state handling in your V0 prompt, since ADP API calls can take a few seconds. After generating the UI, push it to GitHub using V0's Git panel, then connect the repository to Vercel for deployment.
Create an HR dashboard with a top navigation bar showing 'ADP Dashboard' and tabs for Employees, Payroll, and Time & Attendance. On the Employees tab, show a search input and department filter dropdown above a data table. Table columns: Employee Name, Job Title, Department, Location, Status (Active/Inactive badge), and Hire Date. Show pagination controls at the bottom (Previous/Next buttons and page info). Include a total count at the top right of the table. Use loading skeleton rows while data fetches from /api/adp/workers. Show an empty state with a 'No employees found' message when filters return zero results.
Paste this in V0 chat
Pro tip: Ask V0 to generate separate tab components for Employees, Payroll, and Time & Attendance — this lets you build and test each section independently before wiring up all three ADP API endpoints.
Expected result: A multi-tab HR dashboard renders in V0's preview with an employee table, search and filter controls, loading states, and pagination. The component is structured to fetch from /api/adp/workers.
Create the ADP Authentication and API Routes
Create the ADP Authentication and API Routes
Create the Next.js API routes that handle ADP's OAuth 2.0 authentication and proxy requests to ADP's data endpoints. ADP uses the client credentials grant type, where your server posts your client ID and secret to ADP's token endpoint and receives a bearer token in response. This token must be included in all subsequent API calls as an Authorization: Bearer header. Because serverless functions are stateless and a new token request on every API call would be slow and wasteful, implement a simple token caching strategy. In a Next.js serverless context, a module-level variable can cache the token and its expiry time — while this cache is per-function-instance and not shared across instances, it significantly reduces unnecessary token fetches since ADP tokens are typically valid for 30 minutes or more. The workers API endpoint returns an array of worker objects with structured data for each employee including their legal name, job title, department, work location, employment status, and hire date. The payroll API provides pay statement data organized by pay period. Always use the ADP sandbox environment (api.adp.com/sandbox) during development and switch to production only after thorough testing. ADP requires mutual TLS (mTLS) for production API calls, which means you need a certificate and private key in addition to the client credentials — for development with the sandbox, standard TLS works without certificates.
1// app/api/adp/workers/route.ts2import { NextRequest, NextResponse } from 'next/server';34const ADP_AUTH_URL = 'https://accounts.adp.com/auth/oauth/v2/token';5const ADP_API_BASE = 'https://api.adp.com';67// Simple in-memory token cache (per function instance)8let tokenCache: { token: string; expiresAt: number } | null = null;910async function getADPAccessToken(): Promise<string> {11 // Return cached token if still valid (with 60s buffer)12 if (tokenCache && tokenCache.expiresAt > Date.now() + 60_000) {13 return tokenCache.token;14 }1516 const clientId = process.env.ADP_CLIENT_ID!;17 const clientSecret = process.env.ADP_CLIENT_SECRET!;1819 const credentials = Buffer.from(`${clientId}:${clientSecret}`).toString('base64');2021 const response = await fetch(ADP_AUTH_URL, {22 method: 'POST',23 headers: {24 Authorization: `Basic ${credentials}`,25 'Content-Type': 'application/x-www-form-urlencoded',26 },27 body: 'grant_type=client_credentials',28 });2930 if (!response.ok) {31 throw new Error(`ADP auth failed: ${response.status} ${response.statusText}`);32 }3334 const data = await response.json();35 const expiresIn = data.expires_in ?? 1800; // Default 30 minutes3637 tokenCache = {38 token: data.access_token,39 expiresAt: Date.now() + expiresIn * 1000,40 };4142 return data.access_token;43}4445export async function GET(request: NextRequest) {46 const clientId = process.env.ADP_CLIENT_ID;47 const clientSecret = process.env.ADP_CLIENT_SECRET;4849 if (!clientId || !clientSecret) {50 return NextResponse.json(51 { error: 'ADP credentials not configured' },52 { status: 500 }53 );54 }5556 const { searchParams } = new URL(request.url);57 const page = parseInt(searchParams.get('page') ?? '1', 10);58 const pageSize = parseInt(searchParams.get('pageSize') ?? '25', 10);59 const department = searchParams.get('department') ?? '';6061 const skip = (page - 1) * pageSize;62 const queryParams = new URLSearchParams({63 $top: pageSize.toString(),64 $skip: skip.toString(),65 });6667 if (department) {68 queryParams.set('$filter', `department/departmentCode/codeValue eq '${department}'`);69 }7071 try {72 const token = await getADPAccessToken();7374 const response = await fetch(75 `${ADP_API_BASE}/hr/v2/workers?${queryParams.toString()}`,76 {77 headers: {78 Authorization: `Bearer ${token}`,79 'Content-Type': 'application/json',80 },81 }82 );8384 if (!response.ok) {85 throw new Error(`ADP API error: ${response.status}`);86 }8788 const data = await response.json();8990 // Normalize the ADP worker data structure91 const workers = (data.workers ?? []).map((worker: Record<string, unknown>) => {92 const person = worker.person as Record<string, unknown> ?? {};93 const legalName = person.legalName as Record<string, unknown> ?? {};94 const assignment = ((worker.workAssignments as unknown[]) ?? [])[0] as Record<string, unknown> ?? {};9596 return {97 id: worker.associateOID,98 firstName: legalName.givenName,99 lastName: legalName.familyName1,100 jobTitle: (assignment.jobTitle as string) ?? '',101 department: ((assignment.homeOrganizationalUnits as unknown[]) ?? [])[0] as string ?? '',102 workLocation: (assignment.baseRemuneration as string) ?? '',103 status: (assignment.workerTypeCode as Record<string, unknown>)?.shortName ?? 'Active',104 hireDate: (assignment.hireDate as string) ?? '',105 };106 });107108 return NextResponse.json({109 workers,110 totalCount: data['@odata.count'] ?? workers.length,111 page,112 pageSize,113 });114 } catch (error) {115 const message = error instanceof Error ? error.message : 'Unknown error';116 console.error('ADP API error:', message);117 return NextResponse.json(118 { error: 'Failed to fetch employee data', details: message },119 { status: 500 }120 );121 }122}Pro tip: ADP's production API requires mutual TLS (mTLS) with a certificate. For initial development and testing, use the ADP sandbox environment which does not require certificates. Switch to mTLS only when moving to production.
Expected result: GET /api/adp/workers returns a paginated array of normalized employee objects with name, job title, department, status, and hire date pulled live from ADP.
Connect the Dashboard to the API Route and Add Filtering
Connect the Dashboard to the API Route and Add Filtering
Update your V0-generated dashboard components to fetch from the Next.js API routes and handle the response data correctly. The employee directory component should call GET /api/adp/workers with pagination parameters (?page=1&pageSize=25) and update when the user changes page or applies department filters. React's useEffect hook combined with useState for the current page and filter values is the standard pattern for this kind of stateful data fetching in V0-generated client components. Make sure the component is marked 'use client' since it uses interactive state. For the department filter dropdown, you can either hardcode known department names (if your org structure is stable) or add a separate GET /api/adp/departments endpoint that fetches distinct department values. When the user applies a filter, pass it as a query parameter to the API route, which forwards it to ADP's OData filtering syntax. Error handling in the component should display a user-friendly message when the API route fails, rather than an empty table — a simple alert banner or error card works well for internal HR tools. For the payroll and time/attendance sections, create additional API routes following the same pattern: GET /api/adp/pay-statements and GET /api/adp/timecards. Each route authenticates with the same getADPAccessToken function (which you can extract into a shared lib/adp.ts utility), then calls the appropriate ADP endpoint. For complex ADP integrations with multiple API routes and data types, RapidDev's team can help architect the data fetching layer and ensure efficient token management across your serverless functions.
Update the Employees tab to fetch data from /api/adp/workers with query parameters page and pageSize. Store currentPage in useState (default 1) and refetch when it changes. Map the response workers array to table rows showing firstName + lastName, jobTitle, department, status badge (green for Active, gray for Inactive), and hireDate formatted as MMM DD, YYYY. Wire up the pagination Previous/Next buttons to decrement/increment currentPage. Disable Previous when page is 1 and Next when the table has fewer rows than pageSize. Show a loading spinner while fetching and an error banner if the fetch fails.
Paste this in V0 chat
Pro tip: Extract the ADP token fetch logic into a shared utility at lib/adp-auth.ts so all your ADP API routes can import and reuse the same token caching function without code duplication.
Expected result: The employee directory table loads real ADP data, supports page navigation, and shows the correct Active/Inactive status badges. Filtering by department narrows the results correctly.
Configure Environment Variables and Deploy to Vercel
Configure Environment Variables and Deploy to Vercel
Push your code to GitHub and configure ADP credentials in Vercel. Open the Vercel Dashboard, select your project, and go to Settings → Environment Variables. Add ADP_CLIENT_ID with your ADP application client ID and ADP_CLIENT_SECRET with your client secret — both are found in the ADP Developer Portal under your app registration. If you are using the ADP sandbox environment for testing, also add ADP_ENV with the value 'sandbox' so your API routes can conditionally use the sandbox base URL instead of the production URL. Neither variable should have the NEXT_PUBLIC_ prefix since all ADP communication happens server-side. If your production ADP integration requires mTLS certificates, you will need to store the certificate and private key as environment variables or use Vercel's secret files feature — add ADP_CLIENT_CERT and ADP_CLIENT_KEY as base64-encoded PEM strings and decode them in your API route before using Node's https module with the agent option. Set all environment variables for Production, Preview, and Development environments, then save and trigger a redeployment from the Deployments tab. After deployment, open the deployed URL and verify that the employee directory loads correctly with live ADP data. If you see authentication errors, double-check that your client credentials match the environment (sandbox vs. production) and that the ADP app registration is active.
Pro tip: ADP sandbox credentials and production credentials are different — never use sandbox credentials in production or vice versa. Verify which environment your credentials belong to in the ADP Developer Portal before setting Vercel environment variables.
Expected result: The Vercel deployment succeeds and the HR dashboard loads live employee data from ADP. Environment variables are correctly scoped to Production (and Preview/Development as needed for testing).
Common use cases
Employee Directory Dashboard
A searchable, filterable employee directory that pulls live data from ADP, showing department, location, job title, and hire date. HR teams can quickly look up employees across the organization without logging into ADP directly.
Create an employee directory dashboard with a search bar at the top, department filter dropdown, and location filter. Display employees in a data table with columns for name, job title, department, location, and hire date. Each row should have an 'View Details' button. Show a total employee count badge and support pagination with 25 employees per page. Use a clean professional design with a sidebar nav for HR Dashboard, Payroll, and Time & Attendance sections.
Copy this prompt to try it in V0
Payroll Summary Report
An executive-level payroll summary page showing total payroll costs by department, pay period comparison charts, and top earners by role. Finance and HR leadership get real-time payroll insights without needing ADP report exports.
Build a payroll summary dashboard with a pay period selector dropdown at the top. Show three KPI cards: total payroll amount, average salary, and headcount. Include a bar chart showing payroll cost by department and a line chart showing payroll trend over the last 6 pay periods. Display a table of departments sorted by payroll cost descending. Use a finance dashboard style with blue and gray color scheme.
Copy this prompt to try it in V0
Time and Attendance Tracker
A manager-facing time and attendance view showing which employees are on schedule, who has missing timecards, and overtime alerts for the current pay period. Managers get a quick visual summary without navigating ADP's complex time management interface.
Design a time and attendance dashboard for managers. Show a weekly grid view of team members with their scheduled vs. actual hours. Use green for on-track, yellow for approaching overtime, and red for missing timecards. Include a summary card showing total team hours this week, overtime alerts count, and absent employees today. Add a 'Missing Timecard' alert badge on affected employees. Clean table design with status indicators.
Copy this prompt to try it in V0
Troubleshooting
API route returns 401 Unauthorized when calling ADP endpoints
Cause: The ADP access token is expired, the client credentials are incorrect, or the credentials belong to a different environment (sandbox vs. production).
Solution: Verify that ADP_CLIENT_ID and ADP_CLIENT_SECRET in Vercel match your ADP Developer Portal app registration exactly. Check that you're using the correct ADP auth URL for your environment — sandbox uses https://accounts.adp.com/auth/oauth/v2/token and production may use a different endpoint. Redeploy after correcting credentials since Vercel injects environment variables at build time for server routes.
ADP token fetch succeeds but /hr/v2/workers returns 403 Forbidden
Cause: The ADP API client does not have the required scopes or product entitlements enabled for the Workers API. ADP API access is controlled by both OAuth scopes and product subscriptions.
Solution: Log into the ADP Developer Portal and verify your app has the HR API product enabled in addition to the client credentials. Some ADP APIs require an ADP Marketplace subscription — contact your ADP administrator to confirm your organization's API entitlements. In the developer sandbox, ensure you selected the correct API products when creating the app registration.
Employee data returns but names and fields are undefined or nested unexpectedly
Cause: ADP's worker API response has a deeply nested JSON structure — the name is inside person.legalName.givenName, not at the top level. The response shape can also vary based on which ADP modules your organization has enabled.
Solution: Log the raw ADP API response in your API route using console.log and check the Vercel function logs (Vercel Dashboard → Logs) to see the actual response structure. Adjust the property path in your normalization code to match the actual nesting. Use optional chaining (?.) everywhere since ADP response fields are not always populated.
1// Use optional chaining for safe access:2const firstName = worker?.person?.legalName?.givenName ?? 'Unknown';3const jobTitle = worker?.workAssignments?.[0]?.jobTitle ?? 'No Title';Token caching causes stale access token errors after 30 minutes in production
Cause: The module-level token cache variable is per-function-instance. Different Vercel function instances maintain independent caches, and if a cached token expires between requests, the next request from that instance will fail before the cache is invalidated.
Solution: Add a 60-second buffer to the token expiry check (expiresAt > Date.now() + 60_000) so tokens are refreshed before they expire. For higher-traffic production apps, consider storing the token in Vercel KV (Upstash Redis) so all function instances share a single cached token, reducing redundant token fetch calls.
1// Refresh token 60 seconds before expiry:2if (tokenCache && tokenCache.expiresAt > Date.now() + 60_000) {3 return tokenCache.token; // Still valid4}5// Otherwise, fetch a new tokenBest practices
- Always use ADP's sandbox environment for development and testing — never use production credentials in a development or staging environment
- Cache ADP access tokens with a 60-second expiry buffer to avoid re-authenticating on every API call while ensuring tokens never expire mid-request
- Extract ADP authentication logic into a shared lib/adp-auth.ts utility so all API routes import the same token caching function
- Normalize ADP's deeply nested response structure in the API route before returning data to the client — the frontend should receive clean, flat objects rather than the raw ADP JSON
- Store ADP_CLIENT_ID and ADP_CLIENT_SECRET without the NEXT_PUBLIC_ prefix — ADP credentials must never be exposed to the browser
- Add pagination to all ADP list endpoints using ADP's OData $top and $skip parameters to avoid fetching thousands of records in a single request
- Log ADP API errors in your Next.js route using console.error so they appear in Vercel's function logs for debugging without exposing details in API responses
Alternatives
Use QuickBooks instead of ADP if you need accounting and payroll for a small business — QuickBooks is easier to set up and more affordable for teams under 50 employees, while ADP is designed for larger enterprise HR and payroll needs.
Choose FreshBooks if your primary need is freelancer or small business invoicing and time tracking — FreshBooks has a simpler API than ADP and is not designed for multi-employee HR management.
Use Expensify instead of ADP if expense reporting and reimbursement is your focus — Expensify integrates with payroll systems including ADP but specializes in the expense workflow that ADP's own expense tools handle less elegantly.
Frequently asked questions
Does ADP have a free developer tier or sandbox environment?
Yes — ADP provides a free developer sandbox at developers.adp.com where you can register an app and test API calls against sample data without a production ADP subscription. The sandbox uses the same API structure as production but returns mock employee data. You need to create a free developer account to access sandbox credentials.
Do I need mTLS certificates to use the ADP API?
The ADP sandbox environment does not require mutual TLS (mTLS) certificates — standard HTTPS works for development. However, the ADP production API does require mTLS with a client certificate issued by ADP. When you're ready to go to production, you'll need to generate a certificate signing request (CSR) through the ADP Developer Portal and receive a certificate to include with your API calls.
Can V0 generate the ADP integration code automatically?
V0 can generate the Next.js component code and basic API route structure, but ADP's authentication and data normalization require manual implementation since V0 doesn't have detailed knowledge of ADP's specific API endpoints and response structures. Use V0 to build the dashboard UI and API route scaffolding, then manually add the ADP-specific authentication and data fetching logic from this guide.
How do I handle ADP's deeply nested JSON response structure?
ADP responses are deeply nested — a worker's first name is at person.legalName.givenName, not at the top level. The best approach is to normalize the ADP response in your Next.js API route before returning data to the frontend. Create a mapping function that extracts and flattens the fields your UI needs, using optional chaining (?.) for all property accesses since not all fields are always populated.
What ADP API scopes do I need for the Workers API?
The ADP Workers API requires the api:workers:read scope at minimum for reading employee data. For payroll data, you need api:pay:read. For time and attendance, api:time:read is required. Configure these scopes in your ADP Developer Portal app registration and ensure your ADP administrator has enabled the corresponding ADP product subscriptions for your organization's API account.
Can I use ADP data in Vercel Preview deployments?
Yes — configure your ADP sandbox credentials as Preview environment variables in Vercel (separate from Production environment variables). This lets your preview deployments test against ADP's sandbox data without touching production employee records. Set ADP_CLIENT_ID and ADP_CLIENT_SECRET separately for Production and Preview scopes in Vercel Dashboard → Settings → Environment Variables.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation