Skip to main content
RapidDev - Software Development Agency
bubble-integrationsBubble API Connector

ADP

Connecting Bubble to ADP requires a lightweight proxy service (Cloudflare Worker recommended) because ADP mandates mutual TLS (mTLS) client certificate authentication on every API call — including the OAuth2 token exchange — and Bubble's API Connector cannot inject client certificates at the TLS layer. The proxy handles mTLS, caches tokens, and exposes clean REST endpoints that Bubble calls with a simple private API key.

What you'll learn

  • Why ADP requires a proxy service and how mTLS works at a conceptual level for non-technical builders
  • How to register an ADP application in the Developer Portal and obtain mTLS credentials
  • How to deploy a Cloudflare Worker proxy that handles mTLS token exchange and exposes Bubble-friendly endpoints
  • How to configure Bubble's API Connector to call the proxy with a private API key
  • How to handle ADP's deeply nested JSON with Bubble transformers for Workers, Pay, and Time data
  • How to paginate large employee rosters using ADP's OData $top/$skip parameters
Book a free consultation
4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members
Advanced19 min read4–8 hoursHR & RecruitingLast updated July 2026RapidDev Engineering Team
TL;DR

Connecting Bubble to ADP requires a lightweight proxy service (Cloudflare Worker recommended) because ADP mandates mutual TLS (mTLS) client certificate authentication on every API call — including the OAuth2 token exchange — and Bubble's API Connector cannot inject client certificates at the TLS layer. The proxy handles mTLS, caches tokens, and exposes clean REST endpoints that Bubble calls with a simple private API key.

Quick facts about this guide
FactValue
ToolADP
CategoryHR & Recruiting
MethodBubble API Connector
DifficultyAdvanced
Time required4–8 hours
Last updatedJuly 2026

Why ADP requires a proxy — and what that means for your Bubble app

ADP is the most structurally complex integration in the Bubble ecosystem — not because its REST API is difficult to use, but because of how it authenticates. Every single ADP API call, including the OAuth2 token exchange, requires mutual TLS (mTLS): your client presents an X.509 certificate during the TLS handshake before any HTTP request is sent. Bubble's API Connector makes outbound requests through Bubble's shared infrastructure, which cannot inject custom client certificates. The solution is a thin proxy service — typically a Cloudflare Worker — that holds your ADP mTLS credentials, performs the token exchange, caches the token, and exposes clean REST endpoints that Bubble calls with a simple private API key.

Integration method

Bubble API Connector

Bubble's API Connector calls a Cloudflare Worker proxy that handles ADP's mandatory mTLS certificate authentication, OAuth2 token exchange, and token caching — then returns clean JSON that Bubble reads like any standard REST API.

Prerequisites

  • An active ADP subscription for your organization — there is no public sandbox accessible without an ADP account
  • ADP Developer Portal access (developers.adp.com) — registration may require your organization's ADP relationship manager
  • A registered ADP API application with Client ID, Client Secret, mTLS certificate (PEM), and private key downloaded from the Developer Portal
  • A Cloudflare account (free tier sufficient) for deploying the Worker proxy, OR an equivalent server-side hosting service that supports Node.js TLS options
  • Basic comfort with Cloudflare's dashboard or the Wrangler CLI for deploying the Worker — this is the one place where a developer or technical resource is needed
  • A Bubble app on Starter plan ($32/mo) or above if you want scheduled Backend Workflows to sync ADP data to Bubble's database on a nightly schedule

Step-by-step guide

1

Register your ADP application and download mTLS credentials

Navigate to the ADP Developer Portal at developers.adp.com and sign in with your ADP credentials. If you don't yet have Developer Portal access, contact your organization's ADP account manager — access may need to be provisioned separately from your main ADP subscription. Create a new API application in the portal. During registration, select the API products you need access to: Workers API for employee records, Pay API for payroll summaries, and/or Time & Attendance API for time cards. Each product may require separate approval from ADP. Once your application is approved (this can take days to weeks depending on your ADP relationship), the portal provides: - Client ID — a public identifier for your application - Client Secret — a private credential for the OAuth2 token exchange - X.509 Certificate (PEM format) — the public part of your mTLS client certificate - Private Key (PEM format) — the matching private key for the certificate Download all four values and store them immediately in a secure credential store (like 1Password or your organization's secrets manager). The private key especially must never be committed to version control, stored in plain text files, or placed in any system that logs environment variables. Note the API base URLs for your environment: - Production: https://api.adp.com (data), https://accounts.adp.com/auth/oauth/v2/token (token) - Sandbox: https://api.adpapis.com (data), https://accounts.adpapis.com/auth/oauth/v2/token (token) Start with the sandbox environment to verify connectivity before pointing at production data.

Pro tip: ADP's sandbox and production environments are completely separate API products. Credentials (Client ID, Client Secret, certificates) from the sandbox do NOT work against production endpoints. Your ADP Developer Portal account manages both sets of credentials separately.

Expected result: You have four credential components: Client ID, Client Secret, certificate PEM, and private key PEM — all stored securely. You know whether you're targeting the sandbox or production environment.

2

Deploy the Cloudflare Worker proxy with mTLS support

The Cloudflare Worker proxy is the core of this integration. It performs ADP's mTLS OAuth2 token exchange, caches the token for its lifetime (~3,600 seconds), and exposes scoped REST endpoints that Bubble calls with a simple API key header. Create a new Cloudflare Worker in the Cloudflare dashboard (workers.cloudflare.com). Name it something like 'adp-proxy'. In the Worker's environment settings, add the following secret variables (use the 'Add variable' → 'Encrypt' option so they are never visible after saving): - ADP_CLIENT_ID - ADP_CLIENT_SECRET - ADP_CERT_PEM (paste the full certificate PEM including headers) - ADP_PRIVATE_KEY_PEM (paste the full private key PEM including headers) - ADP_PROXY_API_KEY (a random secret string you generate — this is what Bubble uses to authenticate to your proxy) - ADP_TOKEN_ENDPOINT (e.g., https://accounts.adp.com/auth/oauth/v2/token) - ADP_API_BASE_URL (e.g., https://api.adp.com) The Worker code validates the incoming X-Proxy-Key header against ADP_PROXY_API_KEY, checks for a cached token, exchanges credentials for a new token using ADP's mTLS endpoint when needed, caches the token with expiry tracking, and routes requests to the appropriate ADP endpoint based on the URL path (e.g., /workers routes to ADP's Workers API, /pay-summary routes to ADP's Pay API). Once deployed, test the Worker using the Cloudflare dashboard's test interface by sending a GET request to https://your-worker.workers.dev/workers with the X-Proxy-Key header. A successful first response confirms the mTLS handshake, token exchange, and ADP data retrieval are all working.

adp_cloudflare_worker.js
1// Cloudflare Worker proxy for ADP mTLS integration
2// Deploy to Cloudflare Workers — requires secrets: ADP_CLIENT_ID, ADP_CLIENT_SECRET,
3// ADP_CERT_PEM, ADP_PRIVATE_KEY_PEM, ADP_PROXY_API_KEY, ADP_TOKEN_ENDPOINT, ADP_API_BASE_URL
4
5let cachedToken = null;
6let tokenExpiresAt = 0;
7
8async function getAdpToken(env) {
9 const now = Date.now() / 1000;
10 if (cachedToken && now < tokenExpiresAt - 60) {
11 return cachedToken;
12 }
13
14 // ADP requires form-url-encoded body, NOT JSON
15 const body = new URLSearchParams({
16 grant_type: 'client_credentials',
17 client_id: env.ADP_CLIENT_ID,
18 client_secret: env.ADP_CLIENT_SECRET
19 });
20
21 // mTLS: pass cert and key via fetch options (Cloudflare Workers support)
22 const tokenResponse = await fetch(env.ADP_TOKEN_ENDPOINT, {
23 method: 'POST',
24 headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
25 body: body.toString(),
26 // Note: Cloudflare Workers handle mTLS via wrangler.toml mtls_certificates
27 // or via the fetch() cf.clientCertificate option in enterprise plans
28 });
29
30 if (!tokenResponse.ok) {
31 throw new Error(`ADP token exchange failed: ${tokenResponse.status}`);
32 }
33
34 const data = await tokenResponse.json();
35 cachedToken = data.access_token;
36 tokenExpiresAt = now + (data.expires_in || 3600);
37 return cachedToken;
38}
39
40export default {
41 async fetch(request, env) {
42 // Validate proxy API key
43 const proxyKey = request.headers.get('X-Proxy-Key');
44 if (proxyKey !== env.ADP_PROXY_API_KEY) {
45 return new Response('Unauthorized', { status: 401 });
46 }
47
48 const url = new URL(request.url);
49 const path = url.pathname; // e.g. /workers, /pay-summary
50
51 try {
52 const token = await getAdpToken(env);
53 const adpUrl = `${env.ADP_API_BASE_URL}${path}${url.search}`;
54
55 const adpResponse = await fetch(adpUrl, {
56 method: request.method,
57 headers: {
58 'Authorization': `Bearer ${token}`,
59 'Content-Type': 'application/json'
60 },
61 body: request.method !== 'GET' ? await request.text() : undefined
62 });
63
64 const responseData = await adpResponse.json();
65 return new Response(JSON.stringify(responseData), {
66 status: adpResponse.status,
67 headers: { 'Content-Type': 'application/json' }
68 });
69 } catch (err) {
70 return new Response(JSON.stringify({ error: err.message }), {
71 status: 500,
72 headers: { 'Content-Type': 'application/json' }
73 });
74 }
75 }
76};

Pro tip: The OAuth2 token endpoint body for ADP must use application/x-www-form-urlencoded content type — NOT JSON. This is different from most modern APIs. Sending a JSON body causes the token exchange to fail with a 400 error that resembles an invalid credential error.

Expected result: The Cloudflare Worker is deployed and accessible at your-worker.workers.dev. Testing with the correct X-Proxy-Key header returns a 200 response with ADP data (or properly formatted error messages). The Worker's secret environment variables are encrypted and not visible in the dashboard.

3

Configure Bubble's API Connector to call the proxy

With the Cloudflare Worker proxy running, Bubble now calls a standard HTTPS endpoint — no mTLS required from Bubble's side. Open your Bubble app's Plugins tab and install 'API Connector' by Bubble (free, if not already installed). Click 'Add another API' and name it 'ADP Proxy'. Set the base URL to your Cloudflare Worker URL: https://your-worker.workers.dev In the 'Shared headers for all calls' section, add one header: - Key: X-Proxy-Key - Value: the ADP_PROXY_API_KEY secret you set in the Worker - Mark as Private — click the Private checkbox to ensure this key is never sent to the browser Add your first call — the Workers (employee) endpoint: - Name: Get Employees - Method: GET - URL: https://your-worker.workers.dev/workers - Use as: Data - Add URL parameters: - $top: 100 (max records per page) - $skip: 0 (starting offset, make this dynamic for pagination) Click 'Initialize call'. The proxy fetches from ADP and returns employee data. Bubble detects the response fields. ADP's Workers response is deeply nested — common fields to look for after proxy or transformer flattening: - associateOID (ADP's internal employee identifier) - person/legalName/formattedName (full name) - workAssignments[0]/jobTitle - workAssignments[0]/hireDate - workAssignments[0]/assignmentStatus/statusCode/codeValue (ACTIVE, TERMINATED, etc.) Add similar GET calls for pay summaries (/pay-summary) and time cards (/time-cards) following the same pattern.

adp_bubble_connector_config.json
1{
2 "api_name": "ADP Proxy",
3 "base_url": "https://your-worker.workers.dev",
4 "shared_headers": [
5 {
6 "key": "X-Proxy-Key",
7 "value": "<private>",
8 "private": true
9 }
10 ],
11 "calls": [
12 {
13 "name": "Get Employees",
14 "method": "GET",
15 "url": "https://your-worker.workers.dev/workers",
16 "use_as": "Data",
17 "params": [
18 { "key": "$top", "value": "100" },
19 { "key": "$skip", "value": "<dynamic>", "initial": "0" }
20 ]
21 },
22 {
23 "name": "Get Pay Summary",
24 "method": "GET",
25 "url": "https://your-worker.workers.dev/pay-summary",
26 "use_as": "Data",
27 "params": [
28 { "key": "payPeriodStart", "value": "<dynamic>" },
29 { "key": "payPeriodEnd", "value": "<dynamic>" }
30 ]
31 },
32 {
33 "name": "Get Time Cards",
34 "method": "GET",
35 "url": "https://your-worker.workers.dev/time-cards",
36 "use_as": "Data",
37 "params": [
38 { "key": "$top", "value": "100" },
39 { "key": "$skip", "value": "<dynamic>", "initial": "0" },
40 { "key": "weekStart", "value": "<dynamic>" }
41 ]
42 }
43 ]
44}

Pro tip: The X-Proxy-Key header you set here is YOUR secret — you generated it when setting up the Worker. It has nothing to do with ADP's credentials, which live exclusively in the Cloudflare Worker's encrypted environment. Bubble users, including admins with access to the Bubble editor, should not see the raw key value because it is marked Private.

Expected result: The ADP Proxy API group appears in the Bubble API Connector with the X-Proxy-Key header showing the Private lock icon. The 'Get Employees' call initializes successfully, and Bubble detects the ADP employee data fields available from the proxy response.

4

Build the employee directory UI with pagination

With the API Connector calls initialized, build the HR dashboard UI in Bubble's Design editor. Employee directory page: Add a Repeating Group and set its Data source to 'Get data from an external API' → 'ADP Proxy - Get Employees'. Set $skip to (Current page - 1) × 100 for offset pagination (Bubble's page numbers start at 1, so subtract 1 and multiply by your page size). Set $top to 100. Inside the Repeating Group cell, add text elements for each employee field. ADP returns deeply nested JSON — after initialization, look for the auto-detected fields in Bubble's expression builder. If your proxy flattens the fields (recommended), you'll see straightforward fields like name, jobTitle, status, hireDate. If not, navigate nested paths via Bubble's data expression dot notation: Current cell's ADP Proxy Get Employees's workAssignments:first item's jobTitle. Add a status color indicator: conditional formatting on a color indicator element — ACTIVE = green, TERMINATED = red, LEAVE_OF_ABSENCE = orange. Add pagination controls: Previous and Next buttons that change a numeric page custom state. Bind $skip dynamically to (custom_page - 1) × 100. Filter options: Add dropdowns for department and status. For department filtering, pass the department name as an additional OData $filter parameter to the proxy URL if your proxy supports it, or filter client-side using Bubble's ':filtered' operator on the Repeating Group's data source. Privacy: Set up privacy rules in Bubble's Data tab for any Things you create to store ADP employee data. Employee PII (name, salary, employment status) requires strict privacy rules — typically limited to logged-in admins with a specific role.

Pro tip: For large ADP organizations (1,000+ employees), paginating 100 at a time in the UI works, but syncing all records to a Bubble database for full-text search requires a Backend Workflow that loops through pages with 'Schedule API workflow' on the next iteration (paid plan). Plan for Workload Unit consumption — each API call and database write counts toward your WU budget.

Expected result: The employee directory Repeating Group displays employee records from ADP with name, job title, department, hire date, and status. Pagination controls load the next 100 employees. Status indicators show color-coded employment status. Filters work to narrow the displayed list.

5

Add write-back calls and optional nightly sync Backend Workflow

For time-card approval write-backs (PATCH /time-cards/{id}/approval), add a new Action call in the API Connector: - Name: Approve Time Card - Method: POST or PATCH (check your ADP API product documentation — the specific endpoint format varies by ADP product tier) - URL: https://your-worker.workers.dev/time-cards/[id]/approval (with dynamic path param for the time card ID) - Body type: JSON - Body: the approval payload per ADP's specification - Use as: Action IMPORTANT: Write permissions (POST/PATCH to ADP) require explicit write scope in your ADP Developer Portal app registration. If you only requested read access during registration, write calls will return 403 Forbidden. Update your ADP app's permissions in the Developer Portal and confirm approval before building write workflows. Optional nightly sync (requires paid Bubble plan — Starter $32/mo+): Some use-cases benefit from syncing ADP employee data into a Bubble database nightly for faster searches and reporting. Create a Recurring Backend Workflow (Settings → API → Backend Workflows → Recurring) that runs daily at a low-traffic time: 1. Call 'Get Employees' with $skip=0, $top=100 → create or update Bubble Things for each employee 2. Schedule another API workflow with $skip=100 for the next 100 records 3. Continue until no more records are returned This pattern consumes significant Workload Units (one WU per API call + one per database write). For large organizations, evaluate whether the WU cost of nightly sync is worthwhile compared to live API calls on each page load. RapidDev's team has helped enterprise HR teams build ADP-connected Bubble tools with complex sync architectures — free scoping call at rapidevelopers.com/contact.

adp_write_calls_config.json
1{
2 "call_name": "Approve Time Card",
3 "method": "POST",
4 "url": "https://your-worker.workers.dev/time-cards/<dynamic_id>/approval",
5 "use_as": "Action",
6 "body_type": "JSON",
7 "body": {
8 "approvalStatus": "approved",
9 "approvedBy": "<dynamic_manager_id>"
10 }
11}
12
13// ADP OData pagination pattern:
14// Page 1: $top=100&$skip=0
15// Page 2: $top=100&$skip=100
16// Page 3: $top=100&$skip=200
17// Continue until response returns fewer records than $top

Pro tip: ADP's maximum page size is typically 100 records per request ($top=100). For organizations with thousands of employees, a full roster sync requires dozens of sequential API calls — plan the nightly sync workflow carefully and monitor WU usage in Bubble's Logs tab after the first few runs.

Expected result: Time-card approval buttons trigger POST/PATCH calls to the proxy and update ADP records. The nightly sync Backend Workflow runs on schedule and populates or updates the Bubble employee database. Write operations return success responses and the UI refreshes to reflect changes.

Common use cases

Employee headcount and org chart dashboard

HR managers and executives need a real-time view of headcount by department, location, and employment status — without logging into ADP directly. The Bubble app calls the proxy's /workers endpoint, which fetches from ADP's Workers API, and displays results in filterable Repeating Groups. Departmental breakdowns use Bubble's grouped data expressions on the flat worker list.

Bubble Prompt

Build an employee directory page in Bubble that fetches from a proxy endpoint /workers with $top=100 and $skip pagination. Display each employee's name, department, job title, hire date, and assignment status in a Repeating Group. Add filters for department and status. Show a total headcount counter at the top of the page.

Copy this prompt to try it in Bubble

Payroll summary report for finance teams

Finance and operations teams need periodic payroll summaries — total payroll cost by period, earnings by employee category — without giving everyone ADP admin access. The Bubble app reads from the proxy's /pay-summary endpoint (ADP's Pay API), displays data in a tabular Repeating Group, and optionally exports to CSV using Bubble's file download actions.

Bubble Prompt

Create a payroll summary page in Bubble that calls the proxy endpoint /pay-summary with a date range parameter. Show each pay period's total gross pay, deductions, and net pay in a table. Add a date range filter and a 'Download CSV' button that creates a downloadable file from the displayed data.

Copy this prompt to try it in Bubble

Time-card approval workflow

Team managers review and approve employee time cards directly from a Bubble app — reducing the ADP admin burden on HR. The app reads from the proxy's /time-cards endpoint (ADP Time & Attendance API), shows pending time cards by employee, and lets managers approve or reject entries. Write-back approval calls require explicit write permissions in the ADP app registration (separate from read access).

Bubble Prompt

Build a time-card approval page in Bubble. Load pending time cards for the week from proxy endpoint /time-cards with a date range filter. Show each entry's employee name, date, hours, and status in a Repeating Group. Add Approve and Reject buttons that POST to /time-cards/{id}/approval. Refresh the list after each action.

Copy this prompt to try it in Bubble

Troubleshooting

SSL handshake error or TLS connection failure when calling ADP directly from Bubble API Connector

Cause: Attempting to call ADP's API endpoints (api.adp.com or accounts.adp.com) directly from Bubble's API Connector without a proxy. ADP requires mTLS client certificate authentication at the TLS layer — Bubble's infrastructure cannot present a custom X.509 client certificate.

Solution: This is not a configuration error — it is an architectural constraint. Bubble API Connector cannot do mTLS. Deploy the Cloudflare Worker proxy (Step 2) and point Bubble's API Connector at the proxy URL (https://your-worker.workers.dev) rather than at ADP directly. The proxy handles all mTLS communication with ADP.

Proxy returns 401 Unauthorized when Bubble calls it, but the proxy is deployed correctly

Cause: The X-Proxy-Key header value in Bubble's API Connector doesn't match the ADP_PROXY_API_KEY secret set in the Cloudflare Worker's environment.

Solution: In the Cloudflare dashboard, navigate to your Worker → Settings → Variables and verify the ADP_PROXY_API_KEY value (note: encrypted secrets show only '(encrypted)' once saved — you may need to re-set it if you're unsure of the value). In Bubble's API Connector, verify the X-Proxy-Key shared header value matches exactly (case-sensitive, no leading/trailing spaces). Re-initialize the API calls after correcting the header value.

ADP token exchange in the proxy returns 400 Bad Request

Cause: The OAuth2 token request body is being sent as JSON instead of application/x-www-form-urlencoded, OR the ADP_TOKEN_ENDPOINT URL is pointing at the wrong environment (sandbox vs. production).

Solution: Verify the proxy's token exchange call uses Content-Type: application/x-www-form-urlencoded and URLSearchParams() (not JSON.stringify()) for the request body — ADP's /oauth/token endpoint requires form-encoded bodies. Also verify the token endpoint URL: https://accounts.adpapis.com/auth/oauth/v2/token for sandbox, https://accounts.adp.com/auth/oauth/v2/token for production. Check the Cloudflare Worker logs (Workers & Pages → your-worker → Logs) for the exact error response body from ADP.

Initialize call in Bubble shows no fields detected or fields are missing

Cause: ADP responses are deeply nested JSON — fields are buried inside objects like person.legalName.formattedName or workAssignments[0].jobTitle. Bubble's auto-detector may not navigate into deeply nested arrays correctly.

Solution: Add a response flattening step in your Cloudflare Worker proxy. Before returning data to Bubble, map the ADP response to a flat structure: extract associateOID as id, person.legalName.formattedName as name, workAssignments[0].jobTitle as jobTitle, workAssignments[0].hireDate as hireDate, and workAssignments[0].assignmentStatus.statusCode.codeValue as status. Bubble initializes and binds flat objects much more reliably than deeply nested ones.

Write-back calls (time-card approval) return 403 Forbidden

Cause: The ADP application registration in the Developer Portal was approved only for read scope. Write operations (POST/PATCH) require separate write permission approval.

Solution: Go to your ADP Developer Portal application settings and request additional write permissions for the relevant API product (Time & Attendance write, for example). ADP may require a separate approval process for write access. Once approved, update the Worker's secrets if new credentials are issued. Read and write permissions are not bundled together for most ADP API products.

Bubble Backend Workflow for nightly sync consumes unexpected Workload Units and slows down the app

Cause: Paginated employee sync makes dozens of sequential API calls plus one database write per employee record — for a 500-employee organization, that's 5 API calls + 500 database writes per sync cycle, all consuming WUs.

Solution: Add WU monitoring in Bubble's Logs tab (Logs → Usage) to see the actual WU cost of the sync workflow. For large organizations, consider syncing only changed records (if ADP's API supports delta queries for your product tier) rather than a full roster sync. Alternatively, reduce sync frequency (weekly instead of nightly) or implement lazy loading — fetch from ADP in real time on page load and cache results in a custom state for the session.

Best practices

  • Never put ADP credentials, the mTLS certificate, or the private key in Bubble's API Connector — these must live exclusively in the Cloudflare Worker's encrypted secret environment variables, inaccessible after they are saved.
  • Mark the X-Proxy-Key header as 'Private' in Bubble's API Connector. This is your proxy's authentication credential — if it leaks, anyone can call your proxy and query ADP data on your behalf.
  • Build response flattening into the Cloudflare Worker proxy layer. Transform ADP's deeply nested JSON (workAssignments[0].assignmentStatus.statusCode.codeValue) into flat fields (status: 'ACTIVE') before returning data to Bubble — this makes Bubble's initialize step more reliable and simplifies UI bindings.
  • Start with the ADP sandbox environment (api.adpapis.com) for all development and testing. Switch to production (api.adp.com) only after verifying the proxy is working correctly, paginating accurately, and handling errors gracefully.
  • Apply Privacy rules in Bubble's Data tab to any Things storing ADP employee data. Employee records (names, salaries, employment status) are legally sensitive HR data — restrict read access to authenticated admins with appropriate roles.
  • Cache the ADP OAuth2 token in the Cloudflare Worker using an in-memory variable with expiry tracking. ADP tokens are valid for approximately 3,600 seconds — re-fetching a token on every request wastes latency and may trigger rate limits on ADP's token endpoint.
  • Plan Workload Unit budgets before building Backend Workflow syncs. Each API call to the proxy and each database write in a sync loop consumes WUs — a 500-employee nightly sync can consume thousands of WUs per day. Evaluate whether live API calls on page load or a scheduled sync better fits your usage pattern and Bubble plan.
  • Test paginated workflows with a small $top value (10 or 20) during development to verify the pagination logic works before increasing to 100. ADP's $top/$skip pagination with large page sizes can be slow on first request due to ADP query processing time.

Alternatives

Frequently asked questions

What is mTLS and why does ADP require it?

Mutual TLS (mTLS) is a security protocol where both the server AND the client present certificates during a TLS handshake — before any HTTP request is sent. Standard HTTPS only requires the server to present a certificate (so you can verify you're talking to the real server). mTLS adds the requirement that the client also presents a certificate (so the server can verify that only authorized clients are connecting). ADP requires mTLS because payroll and HR data is highly sensitive — even a valid OAuth2 token isn't enough. The combination of certificate + token means that even if someone steals your token, they can't use it without also having your private key.

Is the Cloudflare Worker proxy free to host?

Cloudflare Workers has a free tier that includes 100,000 requests per day and up to 10ms CPU time per request — more than sufficient for a Bubble HR dashboard making occasional API calls throughout the day. The paid plan ($5/month) removes the daily request limit and extends CPU time, which is only needed if you're running a very high-traffic dashboard or complex data transformation logic in the Worker. The mTLS overhead and JSON transformation in the proxy typically runs well within the free tier's limits.

Can I use any other proxy service instead of Cloudflare Workers?

Yes. The Cloudflare Worker approach is recommended for its low latency, simple secrets management, and free tier, but any Node.js-capable hosting service works: Railway, Render, Fly.io, or a small AWS Lambda function. The key requirement is that the platform supports passing custom TLS certificate and key options in HTTP client configurations (Node.js's https module or undici library). PHP-based hosts and serverless platforms that don't expose raw TLS options won't work. The Worker code provided in Step 2 can be adapted to any Node.js environment.

Does ADP have a sandbox environment for testing without real employee data?

Yes, ADP provides a sandbox environment at api.adpapis.com (data) and accounts.adpapis.com (token) with synthetic test data. The sandbox requires the same mTLS certificate authentication as production — you cannot bypass the proxy requirement in the sandbox. You'll typically receive separate sandbox credentials (Client ID, Client Secret, and sometimes a separate test certificate) from the ADP Developer Portal when your sandbox application is approved. Always build and test with sandbox credentials before switching your proxy's environment variables to production values.

How long does it take to get ADP Developer Portal access and API approval?

ADP Developer Portal access and API application approval timelines vary significantly — from a few days if your organization has an established ADP relationship with a dedicated account manager, to several weeks if you're going through the standard registration process. ADP is an enterprise platform, not a self-serve developer tool. Write API access (needed for time-card approvals and similar write-back operations) may require additional review beyond read access. Factor these timelines into your project planning — the Bubble app and Cloudflare Worker can be built in advance, but you need approved credentials to test with real data.

Can I connect Bubble to ADP without any developer help?

Deploying the Cloudflare Worker proxy requires some technical comfort — setting up a Cloudflare account, pasting in the Worker code, and configuring encrypted environment variables. This is manageable for a technical founder or a non-technical founder with an hour of patience and a careful read of Cloudflare's dashboard. However, if the mTLS proxy concept is unfamiliar territory, bringing in a developer for the proxy setup step alone (1-2 hours of work) is a reasonable approach. The Bubble UI and API Connector configuration (Steps 3-5) is fully visual and no-code once the proxy is running.

RapidDev

Talk to an Expert

Our team has built 600+ apps. Get personalized help with your project.

Book a free consultation

Integrations are where projects stall

We wire Bubble integrations like this one every week — auth, webhooks, edge cases included. Fixed-price builds from $13K, delivered in 6–10 weeks.

Talk to an integration engineer

We put the rapid in RapidDev

Need a dedicated strategic tech and growth partner? Discover what RapidDev can do for your business! Book a call with our team to schedule a free, no-obligation consultation. We'll discuss your project and provide a custom quote at no cost.