ADP — Operations
Running, debugging and the known gaps. See flows for how each path works.
Environments
Production is the Canadian Marketplace (ca.apps.adp.com) against project pivot-inc. Partner-level configuration lives in the Self-Service portal (adpapps.adp.com/self-service); listings, endpoints and credentials live on the storefront under Developer → Products.
Three Cloud Run services mount ADP secrets and all three need redeploying when a secret changes: adp, oninitadpintegration, syncadpemployeescronjob.
Secrets
Secret versions are pinned at deploy time. Adding a new version changes nothing until the functions are redeployed — restarting an instance re-reads the pinned version.
Store values with no trailing newline (printf %s, never echo); a captured newline breaks authentication with no useful error. Certificates are base64 of the PEM.
The outbound Basic auth pair must match the listing configuration character for character, or every subscription event returns 401.
Certificate renewal
The production mTLS certificate expires 2027-08-27. Expiry breaks every ADP API call at once, so treat the date as a hard deadline.
gcloud secrets versions access latest --secret ADP_PUBLIC_CERT --project pivot-inc \
| base64 -d | openssl x509 -noout -subject -dates
Debugging
Logs for the adp service carry the request URL and, for outbound calls, {method, url, status, correlationID} under the ADP_API tag. Response bodies are not logged there — ADP payloads carry worker PII and the token request carries the client secret.
The one exception is the pay data input call, which is traced in full under the ADP_TRACE tag: request and response, bodies and headers. It is safe to trace because it carries only associate OIDs, file numbers, hours, rates and earning codes — no names, addresses or government IDs. Credential values are masked.
| Question | Where to look |
|---|---|
| Did the event reach us? | adp logs, filter by /subscription/ |
| Did Basic auth match? | A 401 on a subscription route |
| Was the event actionable? | ignored non-actionable eventUrl means no usable eventUrl |
| Did consent land? | ADP consumer application credential not found (consent not given) |
| Did the sync link anyone? | Adding N new integration IDs vs No new integration IDs to add |
| Did the export succeed? | POST /events/payroll/v1/pay-data-input.modify status |
| What exactly did we send, and what came back? | ADP_TRACE tag — the full request and response for the pay data input call, headers included |
A green Integration Report means endpoints are reachable and the Basic auth matched. It does not mean events are processed — the handler deliberately acknowledges ADP's probe, which carries no usable eventUrl.
The payroll worksheet
Each export opens a new batch. eventContext/payrollProcessingJobID is ADP's Batch ID, and ADP appends generated characters when the name is already in use, so the Paydata page accumulates one batch per export. Pivot names them Pivot {period start} {HHmmss}, where the time matches the ADP_TRACE entry — that is how a batch in Workforce Now maps back to the run that created it and its adp-correlationid.
A payee can be accepted and still never appear
An employee whose position start date falls after the open pay cycle is accepted by the API and silently omitted from the worksheet grid. ADP's own guidance states the rule: a future hire is processed only if the position start date is in the current payroll cycle.
Nothing in the response says so. Verified against the A8L sandbox:
| File # | Position start | Worksheet row |
|---|---|---|
| 754010 | 2022-06-01 | renders |
| 023261 | 2023-10-01 | renders |
| 754008 | 2025-02-18 | blank |
| 754009 | 2025-02-21 | blank |
For the omitted employees the request returns 200, the response echoes the payee with the correct hours and rate, the batch totals include their hours, and the footer counts them under "Total Employees (by File#) in this batch". Only the grid row is empty — no file number, no name, no values.
This is a property of the tenant's pay cycle, not of the payload. Diffing the two workers' /hr/v2/workers records field by field shows no structural difference, because the rule keys on the pay cycle rather than on anything the HR record exposes.
When a row is missing, check the employee's position start date against the open pay cycle before suspecting the payload.
Known gaps
| Gap | Effect |
|---|---|
| Employee import not built | ADP workers are never created in Pivot; only name-matched existing employees link (PIVOT-2428, PIVOT-3440) |
| Core / eCommerce listing unbuilt | subscriptionCreate 404s for a company that does not already exist, so a Core listing would fail on first order |
| Assignment is record-only | Unassigning a user does not revoke their access |
| Cancel leaves employee mappings | EmployeeIntegrationIds/{companyId}/adp has been observed surviving a cancel, so a cancelled company can retain a usable SSO mapping |
getEarningCodes has no caller | Earning codes cannot be configured from the UI |
| OAuth tokens are not cached | A fresh token is minted per request; ADP's integration standards require reuse within the 60-minute TTL |
| New hires export as a silent no-op | A payee whose position start date is after the open pay cycle is accepted, counted in the batch totals, and absent from the worksheet. Pivot reports the export as successful |
Employees without a payrollFileNumber are skipped | mapPayrollToAdp drops them with no log, so a mapped employee can be missing from every export without any signal |
| Matcher never re-verifies | An employee with an existing mapping is skipped forever, including a mapping to an associate from a previous subscription |
Deploying
Production deploys only from a manual workflow_dispatch of the Deploy workflow on production. A merge does not deploy, and a push to another branch never touches production.