# ADP

- Tool: FlutterFlow
- Difficulty: Advanced
- Time required: 3–5 hours
- Last updated: July 2026

## TL;DR

Connect FlutterFlow to ADP using a Firebase Cloud Function proxy — ADP requires OAuth2 client-credentials plus a mutual TLS (mTLS) X.509 client certificate on every request, which a Flutter client app cannot present. Deploy the Firebase Function to hold your cert and client secret, expose clean endpoints for Workers, Pay, and Time data, then call those endpoints from FlutterFlow API Calls.

## Why ADP Requires a Backend Proxy in Every FlutterFlow App

ADP Workforce Now and RUN Powered by ADP expose their employee records, payroll summaries, and time-card data through a REST API — but with an unusual security requirement: every single request, including the initial OAuth2 token exchange, must be accompanied by a mutual TLS (mTLS) X.509 client certificate that ADP validates against your registered application. This is on top of the standard OAuth2 client-credentials flow with a client ID and secret. The certificate and its private key are enterprise-grade secrets that must never leave a controlled server environment.

FlutterFlow compiles your project into a native Flutter app that runs on the user's device (iOS, Android, or web). A client app cannot initiate or modify TLS handshakes, and shipping a private key inside an app bundle would expose it to anyone who decompiles the app. This means there is no client-only path to ADP: you need a server-side intermediary regardless of how the FlutterFlow API Call is configured. The right tool for this in the Firebase/FlutterFlow ecosystem is a Firebase Cloud Function. The Function holds your ADP credentials — client ID, client secret, and the mTLS certificate and key — performs the token exchange over mTLS, caches the ~1-hour access token, and exposes narrow, scoped endpoints (Workers, Pay, Time) that your FlutterFlow app calls with normal HTTPS requests.

ADP is an enterprise HCM platform accessed through ADP's Developer Portal and Marketplace. There is no public self-serve free tier for the production API — you connect to it through your existing ADP subscription or by registering as an ADP Marketplace partner. ADP does provide a sandbox environment with synthetic data that mirrors the production API structure, and you should build and validate the entire mTLS chain in sandbox before pointing the Function at production payroll data. Because you're dealing with highly sensitive employee and payroll information, the Firebase Function should also enforce role-scoping — ensuring managers can only see their own team's data — and raw pay figures should not be stored in broadly readable App State variables or Firestore collections.

## Before you start

- An active ADP subscription with API access or an approved ADP Marketplace partner/developer account
- Access to ADP's Developer Portal (developers.adp.com) to register your application and download your mTLS certificate
- A Firebase project with Blaze (pay-as-you-go) plan enabled so you can deploy Cloud Functions
- A FlutterFlow project connected to Firebase (or at minimum able to make outbound HTTPS API Calls)
- Basic familiarity with the FlutterFlow API Calls panel and App State variables

## Step-by-step guide

### 1. Register your app in ADP Developer Portal and obtain credentials

Go to developers.adp.com and log in with your ADP credentials. Navigate to 'My Apps' and click 'Create New App'. Give your app a name that identifies your FlutterFlow project and select the ADP APIs you need access to — typically 'Workers/v2' for employee records, 'Pay/v1' for payroll data, and 'Time and Attendance' for time cards. The exact product availability depends on your ADP subscription tier.

Once the app is created, ADP will generate a Client ID and Client Secret for you. Copy both values immediately — you will store them in your Firebase Cloud Function's environment variables (never in your FlutterFlow project). Next, you need to generate or upload an mTLS X.509 client certificate. ADP's Developer Portal provides instructions for generating a self-signed certificate using OpenSSL, or you can upload a certificate signed by your organization's CA. You'll receive back the certificate registered to your ADP app — download both the certificate (.pem or .crt) and keep your private key (.key) securely. These two files must stay together in your Firebase Cloud Function and must never be committed to a public repository.

Finally, note the sandbox base URL (https://accounts.adpapis.com for token exchange, https://api.adpapis.com for data endpoints) — you will use this during development. The production URLs are https://accounts.adp.com/auth/oauth/v2/token (token) and https://api.adp.com (data). ADP's sandbox provides synthetic employee data that mirrors the production API structure, making it safe to build and test the full mTLS flow without touching real payroll records.

**Expected result:** You have a Client ID, Client Secret, mTLS certificate (.pem), and private key (.key) downloaded from ADP Developer Portal, and you know your sandbox API base URLs.

### 2. Deploy a Firebase Cloud Function with mTLS token exchange

This is the core of the ADP integration. Open the Firebase console, go to your project, and navigate to Functions. You'll write a Cloud Function in Node.js that: (1) reads the ADP client ID, client secret, certificate, and private key from environment variables; (2) makes an HTTPS POST to ADP's token endpoint using the mTLS client certificate; (3) caches the returned access token with its expiry time; and (4) exposes REST endpoints that FlutterFlow will call.

The Node.js https module supports client certificates via the cert and key options in the request configuration. Create a POST request to https://accounts.adpapis.com/auth/oauth/v2/token (sandbox) with grant_type=client_credentials in the body, Authorization: Basic base64(clientId:clientSecret) in the header, and the cert/key pair in the TLS options. ADP returns an access token valid for approximately 3,600 seconds.

Cache this token in a module-level variable with its expiry timestamp so that subsequent calls within the Function instance reuse it without a new token exchange. If the token is expired or missing, fetch a fresh one before proxying the ADP request. Expose separate endpoints in the Function for each ADP resource your app needs: /workers for GET /events/core/v1/workers, /pay-summary for GET /payroll/v1/workers/{associateOID}/pay-distributions, and /time-cards for the Time and Attendance endpoints. In each endpoint, add role-scoping logic — check a manager ID passed from the FlutterFlow app against ADP's workAssignments to return only the requesting manager's team.

ADP responses are deeply nested. Flatten them in the Function before returning to FlutterFlow: extract the fields your widgets need (associate OID, legal name, job title, hire date, employment status) into a simple array of objects. This makes JSON Path mapping in FlutterFlow trivial.

```
// Firebase Cloud Function: ADP proxy with mTLS (index.js)
const functions = require('firebase-functions');
const https = require('https');
const querystring = require('querystring');

// Load credentials from environment config
const ADP_CLIENT_ID = functions.config().adp.client_id;
const ADP_CLIENT_SECRET = functions.config().adp.client_secret;
const ADP_CERT = functions.config().adp.cert_pem;     // PEM string
const ADP_KEY = functions.config().adp.private_key;   // PEM string
const ADP_SANDBOX = true; // set false for production

const TOKEN_HOST = ADP_SANDBOX
  ? 'accounts.adpapis.com'
  : 'accounts.adp.com';
const API_HOST = ADP_SANDBOX
  ? 'api.adpapis.com'
  : 'api.adp.com';

// Simple in-memory token cache
let cachedToken = null;
let tokenExpiry = 0;

async function getADPToken() {
  if (cachedToken && Date.now() < tokenExpiry - 60000) {
    return cachedToken;
  }
  const credentials = Buffer.from(
    `${ADP_CLIENT_ID}:${ADP_CLIENT_SECRET}`
  ).toString('base64');
  const body = querystring.stringify({ grant_type: 'client_credentials' });

  return new Promise((resolve, reject) => {
    const options = {
      hostname: TOKEN_HOST,
      path: '/auth/oauth/v2/token',
      method: 'POST',
      cert: ADP_CERT,
      key: ADP_KEY,
      headers: {
        'Authorization': `Basic ${credentials}`,
        'Content-Type': 'application/x-www-form-urlencoded',
        'Content-Length': Buffer.byteLength(body),
      },
    };
    const req = https.request(options, (res) => {
      let data = '';
      res.on('data', (chunk) => (data += chunk));
      res.on('end', () => {
        const json = JSON.parse(data);
        cachedToken = json.access_token;
        tokenExpiry = Date.now() + json.expires_in * 1000;
        resolve(cachedToken);
      });
    });
    req.on('error', reject);
    req.write(body);
    req.end();
  });
}

async function adpGet(path, token) {
  return new Promise((resolve, reject) => {
    const options = {
      hostname: API_HOST,
      path,
      method: 'GET',
      cert: ADP_CERT,
      key: ADP_KEY,
      headers: { 'Authorization': `Bearer ${token}` },
    };
    const req = https.request(options, (res) => {
      let data = '';
      res.on('data', (chunk) => (data += chunk));
      res.on('end', () => resolve(JSON.parse(data)));
    });
    req.on('error', reject);
    req.end();
  });
}

exports.adpWorkers = functions.https.onRequest(async (req, res) => {
  res.set('Access-Control-Allow-Origin', '*');
  if (req.method === 'OPTIONS') { res.status(204).send(''); return; }
  try {
    const token = await getADPToken();
    const data = await adpGet('/events/core/v1/workers', token);
    const workers = (data.workers || []).map((w) => ({
      associateOID: w.associateOID,
      name: w.person?.legalName?.formattedName || '',
      jobTitle: w.workAssignments?.[0]?.jobTitle || '',
      hireDate: w.workAssignments?.[0]?.hireDate || '',
      status: w.workAssignments?.[0]?.assignmentStatus?.statusCode?.codeValue || '',
    }));
    res.json({ workers });
  } catch (err) {
    res.status(500).json({ error: err.message });
  }
});
```

**Expected result:** Deploying the Firebase Function and calling the /adpWorkers endpoint in a REST client (like Postman or curl from your terminal) returns a JSON array of flattened worker objects. The Function handles all mTLS and OAuth2 complexity transparently.

### 3. Create FlutterFlow API Group pointing at your Firebase Function

Open your FlutterFlow project and click API Calls in the left navigation panel. Click the blue '+ Add' button and choose 'Create API Group'. Name it 'ADP' or 'ADP HR'. In the Base URL field, enter the HTTPS URL of your deployed Firebase Cloud Function — it will look like https://us-central1-YOUR-PROJECT.cloudfunctions.net. Leave authentication set to No Auth for now; your Firebase Function itself handles all ADP auth internally, so FlutterFlow only needs to make a plain HTTPS GET request.

Click '+ Add API Call' inside the ADP group to create your first endpoint. Name it 'Get Workers'. Set the HTTP Method to GET and the endpoint path to /adpWorkers (or whatever route name you used in the Function). On the Variables tab, add a variable called managerId (String type) if you want to pass the current user's manager ID for scoping — include it as a query parameter: ?managerId={{ managerId }}. This allows the Function to filter results to the manager's team.

Move to the Response & Test tab. In the test section, click 'Test API Call' — if your Function is deployed correctly you'll see a JSON response with the workers array. Click 'Generate from Response' to have FlutterFlow automatically generate JSON Path variables for each field: workers[0].associateOID, workers[0].name, workers[0].jobTitle, etc. These JSON Path mappings are what you'll bind to text widgets, list tiles, and DataTable cells in your UI.

Repeat this process to add a 'Get Pay Summary' API Call (endpoint /adpPaySummary) and a 'Get Time Cards' API Call (endpoint /adpTimeCards) within the same ADP API Group. Having all three endpoints in one group keeps the base URL in one place — if you later switch from sandbox to production, you only update the group's Base URL, not every individual call.

```
// Example API Call config (what FlutterFlow stores internally)
{
  "group_name": "ADP",
  "base_url": "https://us-central1-YOUR-PROJECT.cloudfunctions.net",
  "calls": [
    {
      "name": "Get Workers",
      "method": "GET",
      "path": "/adpWorkers",
      "variables": [
        { "name": "managerId", "type": "String", "location": "query" }
      ],
      "json_paths": [
        "$.workers[*].associateOID",
        "$.workers[*].name",
        "$.workers[*].jobTitle",
        "$.workers[*].hireDate",
        "$.workers[*].status"
      ]
    },
    {
      "name": "Get Pay Summary",
      "method": "GET",
      "path": "/adpPaySummary",
      "variables": [
        { "name": "associateOID", "type": "String", "location": "query" }
      ]
    },
    {
      "name": "Get Time Cards",
      "method": "GET",
      "path": "/adpTimeCards",
      "variables": [
        { "name": "associateOID", "type": "String", "location": "query" },
        { "name": "payPeriod", "type": "String", "location": "query" }
      ]
    }
  ]
}
```

**Expected result:** The ADP API Group appears in your left nav with three API Calls (Get Workers, Get Pay Summary, Get Time Cards). Testing Get Workers in the Response & Test tab returns real-looking data from your Firebase Function, and FlutterFlow has generated JSON Path variables for the workers array fields.

### 4. Build the HR dashboard UI and bind ADP data to widgets

Now that your API Calls are configured and returning data, it's time to build the actual screens. Start with the main Headcount screen: in FlutterFlow's canvas, add a Column layout at the top for summary stats (total employees, active count) and a ListView below it for the employee list.

Select the ListView and open its Backend Query settings (the database icon in the right panel). Choose 'API Call' as the data source, select 'ADP → Get Workers', and pass your App State variable containing the current user's manager ID as the managerId variable. FlutterFlow will automatically call Get Workers when the screen loads and pass the response into the ListView's children.

For each list item, use the response JSON Path variables to populate a ListTile widget: set the title text to workerName (JSON path $.workers[*].name), the subtitle to workerJobTitle ($.workers[*].jobTitle), and add a trailing status badge that changes color based on workerStatus. Tap on a list item should navigate to a Worker Detail page passing the associateOID as a page parameter.

On the Worker Detail page, use a Backend Query with Get Pay Summary, passing the associateOID from the page parameter. Display gross pay, deductions, and net pay in Card widgets. Add a second tab for Time Cards using Get Time Cards in another Backend Query — render the daily hours in a compact table using a DataTable widget with columns for date, regular hours, overtime hours, and approval status.

For any actions that write back to ADP (like approving a time card), add an Action to the approve button: open the Action Flow Editor, add a Backend/API Call action pointing to your approval endpoint in the Function, then chain an alert dialog to confirm success or show an error message. Keep confirmation dialogs for any write operations — payroll data mutations are irreversible.

**Expected result:** You have a functional Headcount screen showing your team's employee list loaded from ADP, with working navigation to a detail page showing pay summary and time-card data. Approve buttons trigger write-back actions through the Firebase Function.

### 5. Handle token caching, errors, and role scoping in the Firebase Function

Before going to production, harden the Firebase Function for the realities of a live HR app. The most important concern is token management: ADP access tokens expire after approximately one hour (3,600 seconds). If the cached token expires between requests, the next request will fail with a 401 until a fresh token is fetched. The code example in Step 2 includes basic in-memory caching with a 60-second buffer — this works within a single Function instance, but Firebase Cloud Functions can spin up multiple instances under load, and each instance maintains its own cache. For a production HR app with many concurrent users, use Firebase's own Firestore or Realtime Database to share the cached token across instances: write the token and expiry timestamp to a Firestore document called adp_token/current, and read from it at the start of each request. Only perform a new token exchange if the stored token is within 2 minutes of expiry.

For role scoping, add a Firebase Authentication check at the top of each Function endpoint: verify the Firebase ID token passed in the Authorization header of the FlutterFlow request, look up the authenticated user's UID in a Firestore collection that maps UIDs to ADP manager IDs (populate this during your user onboarding flow), and use that manager ID to filter the ADP Workers response. Never trust a managerId parameter sent directly from the FlutterFlow app — an attacker can change query parameters. Always derive the scope server-side from the authenticated user's identity.

For error handling, return structured error objects from the Function with an HTTP status code matching the ADP error: 401 if the ADP token is invalid, 403 if the user lacks permission for that data scope, 429 if ADP's rate limit is reached (check per your contract — rate limits vary by product and tier). In FlutterFlow, handle these in the Action Flow Editor using the 'On Error' branch of the Backend/API Call action: show a Snack Bar with a user-friendly message and log the raw error to Firestore for debugging.

If you'd rather skip building and maintaining this proxy infrastructure, RapidDev's team sets up FlutterFlow integrations like this every week — including ADP mTLS proxies — with a free scoping call at rapidevelopers.com/contact.

```
// Shared token cache using Firestore (production-grade)
const admin = require('firebase-admin');
admin.initializeApp();
const db = admin.firestore();

async function getADPToken() {
  const tokenDoc = await db.doc('adp_token/current').get();
  const cached = tokenDoc.data();
  if (cached && cached.token && cached.expiry > Date.now() + 120000) {
    return cached.token;
  }
  // Fetch a fresh token (mTLS exchange as in Step 2)
  const freshToken = await fetchFreshADPToken();
  await db.doc('adp_token/current').set({
    token: freshToken.access_token,
    expiry: Date.now() + freshToken.expires_in * 1000,
  });
  return freshToken.access_token;
}

// Role scoping: verify Firebase ID token and look up manager scope
async function getManagerScope(req) {
  const idToken = req.headers.authorization?.replace('Bearer ', '');
  if (!idToken) throw new Error('Unauthenticated');
  const decoded = await admin.auth().verifyIdToken(idToken);
  const userDoc = await db.doc(`hr_users/${decoded.uid}`).get();
  if (!userDoc.exists) throw new Error('User not in HR system');
  return userDoc.data().adpManagerId;
}
```

**Expected result:** The Firebase Function now scopes all ADP data to the authenticated manager's team, shares the cached token across instances via Firestore, and returns structured error responses that FlutterFlow can handle gracefully.

### 6. Validate against ADP sandbox, then switch to production

Before pointing your Firebase Function at live payroll data, complete a full end-to-end validation against ADP's sandbox environment. ADP's sandbox uses different base URLs (accounts.adpapis.com for token, api.adpapis.com for data) and provides synthetic employee records that mirror the production data structure — the same JSON shapes, the same nested fields, the same mTLS requirement. This means everything you've built and tested in development will translate directly to production with only a URL change.

Go through each screen of your FlutterFlow app and verify every API Call returns data, every JSON Path mapping displays the correct value in the widget, and every error scenario (no data, rate limit hit, invalid manager scope) shows the right user message. Check that the time-card approval flow completes successfully end-to-end: the Function receives the approval request, calls ADP's Time and Attendance endpoint, and returns a success status that the FlutterFlow action flow handles.

When you're satisfied with the sandbox validation, switch the Firebase Function to production: update the ADP_SANDBOX constant to false (or use a Function environment variable like ENVIRONMENT=production). Update the Function to use https://accounts.adp.com/auth/oauth/v2/token for the token exchange and https://api.adp.com for data endpoints. Redeploy the Function. In FlutterFlow, your API Calls don't need to change — they still point at the same Firebase Function URL; only the Function's internal routing changes.

Run one final smoke test in the production-connected app against real ADP data with a test manager account before rolling out to your full team. Confirm that the role scoping is working correctly — each manager should see only their direct reports, not the entire company's workforce or payroll data. Monitor the Firebase Function's logs in the first 24 hours for any unexpected errors, rate limit warnings, or token refresh failures.

**Expected result:** Your FlutterFlow app is fully connected to live ADP data, role-scoped to each manager's team, with the Firebase Function handling all mTLS authentication. Managers can view employee records, pay summaries, and time cards from their mobile device.

## Best practices

- Never place the ADP client secret or mTLS private key in a FlutterFlow API Call header or a Dart Custom Action — both end up in the compiled app bundle. They belong exclusively in the Firebase Cloud Function's environment configuration.
- Cache the ADP access token in Firestore (not just in-memory) so all Firebase Function instances share the same token — this prevents redundant mTLS token exchanges and avoids rate limiting the token endpoint.
- Enforce role scoping server-side in the Firebase Function by verifying the caller's Firebase ID token and looking up their allowed ADP manager ID in your database — never trust a managerId parameter sent from the FlutterFlow client.
- Do not store raw pay figures (gross salary, net pay, bank account details) in broad App State variables or in Firestore collections without row-level access controls — payroll data requires the same care as PHI in healthcare apps.
- Build and fully validate the mTLS token exchange in ADP's sandbox environment before switching the Function to production URLs — sandbox uses synthetic data and prevents any risk of accidentally querying or modifying live payroll records.
- Flatten ADP's deeply nested JSON responses in the Firebase Function (not in FlutterFlow) — extracting legalName.formattedName and workAssignments[0].jobTitle server-side gives your FlutterFlow widgets clean, shallow JSON that's easy to bind without complex JSON Path expressions.
- Add structured error handling for ADP's known error codes (401 for auth failure, 403 for scope errors, 429 for rate limits) in both the Firebase Function and the FlutterFlow Action Flow — surface user-friendly messages instead of raw error objects.
- Set up Firebase Function monitoring alerts for error rates and response time spikes — an ADP cert expiry or rate limit issue will appear in Function logs before it surfaces as a user complaint.

## Use cases

### Manager self-service HR dashboard

A FlutterFlow mobile app lets team managers view their direct reports' profiles, job titles, and contact details pulled live from ADP Workers API. Managers can check headcount, see active vs. terminated status, and review employment dates — all scoped to their own team via the Firebase Function's role filter.

Prompt example:

```
Build a screen that lists all employees in the current manager's department, showing name, job title, hire date, and employment status, with a detail page for each employee.
```

### Payroll summary viewer for HR admins

An internal FlutterFlow app surfaces pay period summaries from ADP's Pay API so HR administrators can quickly verify gross pay, deductions, and net pay for the most recent payroll run without logging into the ADP web portal. The Firebase Function queries the Pay API and returns a flattened list that FlutterFlow renders in a DataTable widget.

Prompt example:

```
Create a payroll summary screen showing each employee's gross earnings, total deductions, and net pay for the last completed pay period, with the ability to filter by department.
```

### Time-card approval workflow

A FlutterFlow app pulls pending time-card entries from ADP's Time and Attendance API via the Firebase Function, displays them in a review list, and lets managers approve or flag exceptions directly from their phone. Approved statuses are written back through the Function, keeping ADP as the system of record.

Prompt example:

```
Build a time-card review screen that shows each team member's hours by day for the current week, with approve and flag buttons that update the status in ADP.
```

## Troubleshooting

### Firebase Function returns 'Error: socket hang up' or 'ECONNRESET' when calling ADP token endpoint

Cause: The mTLS handshake is failing. This happens when the certificate PEM or private key stored in the Function's environment config is malformed — common causes include newline characters being escaped as \n in the config string, or the cert and key being swapped.

Solution: Log the first 50 characters of ADP_CERT and ADP_KEY in the Function to verify they start with '-----BEGIN CERTIFICATE-----' and '-----BEGIN PRIVATE KEY-----' respectively. If newlines are escaped, replace them: const cert = ADP_CERT.replace(/\\n/g, '\n'). Test the raw cert/key pair locally using curl: curl --cert client.pem --key client.key -d 'grant_type=client_credentials' https://accounts.adpapis.com/auth/oauth/v2/token

```
const ADP_CERT = functions.config().adp.cert_pem.replace(/\\n/g, '\n');
const ADP_KEY = functions.config().adp.private_key.replace(/\\n/g, '\n');
```

### ADP token endpoint returns HTTP 401 'invalid_client'

Cause: The Client ID or Client Secret is incorrect, or they belong to a different ADP environment (sandbox credentials used against production, or vice versa). Also occurs if the app registration in ADP Developer Portal was not fully approved.

Solution: Verify the Client ID and Client Secret in ADP Developer Portal under My Apps → your app → Credentials. Confirm the Function is pointing at the correct token endpoint: accounts.adpapis.com for sandbox, accounts.adp.com for production. Check the ADP Developer Portal for any pending approval steps on your app registration — some API products require explicit enablement after registration.

### FlutterFlow API Call returns data in the test tab but nothing appears in the app widget

Cause: The JSON Path expression doesn't match the structure of the data the Function returns. ADP responses use deeply nested objects; if the Function returns a different shape than what was used to generate the JSON Paths, the bindings produce empty strings.

Solution: In the FlutterFlow API Calls panel, go to the Response & Test tab for the affected call, click 'Test API Call' to get a live response, then click 'Regenerate JSON Paths' to rebuild the mappings from the current response structure. Check that the widget's data binding dropdown shows the correct JSON Path variable selected — re-select it if it shows as blank.

### API Call to Firebase Function returns 403 'Unauthenticated' in production but works in test

Cause: The FlutterFlow app is not passing the Firebase ID token in the request Authorization header when calling the Function's protected endpoints. In the test tab, FlutterFlow doesn't enforce authentication, but in the live app the Function's getManagerScope() check rejects the request.

Solution: Add a variable to the API Call named idToken (String), set it as a header: Authorization: Bearer {{ idToken }}. In your Action Flow (the action that triggers the API Call), populate idToken using the 'Get Firebase Auth Token' action available under Utilities → Firebase. Chain the Get Token action before the Backend API Call action so the token is ready when the request fires.

### Workers list loads correctly but Pay Summary returns empty or 'worker not found'

Cause: ADP uses internal associate OIDs (alphanumeric strings like 'G3349LLWN2J5T') as the primary key, not employee names or email addresses. If the associateOID is not being passed correctly from the Workers list to the Pay Summary call, ADP returns an empty or error response.

Solution: On the Worker Detail page, verify the page parameter is named associateOID and has type String. In the Get Pay Summary API Call's Variables tab, confirm the associateOID variable is mapped to the page parameter (not a hardcoded test value). Check the Firebase Function's Pay Summary endpoint — it should include the associateOID in the ADP API path: /payroll/v1/workers/{associateOID}/pay-distributions.

## Frequently asked questions

### Can I connect FlutterFlow directly to the ADP API without a Firebase Function?

No — and this is not a workaround, it's a fundamental limitation. ADP requires mutual TLS (mTLS), which means the API client must present an X.509 certificate during the TLS handshake. FlutterFlow compiles to a Flutter client app that runs on the user's device, which cannot participate in mTLS handshakes. Even if it could, shipping an mTLS private key in an app bundle would expose it to anyone who decompiles the app. The Firebase Cloud Function proxy is the only viable path.

### Do I need an ADP developer account, or can I use my existing ADP subscription?

You need to register as an ADP API developer through the ADP Marketplace or ADP Developer Portal, which is separate from your standard ADP subscription. Some ADP plans include API access as part of the subscription; others require an upgrade or a Marketplace partner agreement. Contact your ADP account representative to confirm what API products you're licensed for and to get started with the Developer Portal registration.

### How long does the ADP mTLS certificate last, and what happens when it expires?

mTLS client certificates typically have a validity period set during generation — commonly one to three years, though ADP may specify requirements in your developer agreement. When the certificate expires, the Firebase Function's mTLS handshake will fail immediately and all API calls will return a connection error. Set a reminder in your calendar well before the expiry date, regenerate the certificate in ADP Developer Portal, update the Function's environment config, and redeploy. This is a zero-downtime update as long as you deploy before expiry.

### Can I use the ADP integration in FlutterFlow's web Run mode or browser preview?

Yes — because FlutterFlow is making standard HTTPS requests to your Firebase Cloud Function (not to ADP directly), the web preview works fine for testing. All the mTLS complexity happens inside the Function. The only scenario where you need a device build is if you add Okta or Auth0 OIDC login alongside ADP — those SDK-based login flows require a system browser redirect that doesn't complete in the web preview.

### Can FlutterFlow write data back to ADP (approve time cards, update employee records)?

Yes, if your ADP API access includes write permissions for the relevant products (Time and Attendance approval, HR record updates). Add POST or PATCH endpoints to your Firebase Function for the write operations, create corresponding FlutterFlow API Calls, and trigger them from button actions in the Action Flow Editor. Always add a confirmation dialog before executing writes, and check your ADP API agreement for which write operations are included in your licensed products.

### How do I make sure one manager can't see another team's payroll data?

Role scoping must be enforced in the Firebase Function — never in FlutterFlow, where parameters can be manipulated by a savvy user. When the Function receives a request, it should verify the caller's Firebase ID token (from the Authorization header), look up the authenticated user's assigned ADP manager ID in a secure Firestore collection, and use that manager ID to filter ADP Workers and Pay API responses. Don't accept a managerId directly from the request query string for authorization purposes.

---

Source: https://www.rapidevelopers.com/flutterflow-integrations/adp
© RapidDev — https://www.rapidevelopers.com/flutterflow-integrations/adp
