Workday is a cloud-only SaaS platform providing HCM, Financials, Payroll, and Planning modules. All tenants receive the same software release twice per year (R1 and R2). Authentication methods apply uniformly across all editions. This card covers all three authentication paths: ISU with password for SOAP, OAuth 2.0 (Authorization Code, JWT Bearer, Client Credentials) for REST, and X.509 certificate-based WS-Security for password-free SOAP.
| Property | Value |
|---|---|
| Vendor | Workday |
| System | Workday HCM / Financials (2025R1, 2025R2) |
| API Surface | REST (JSON) + SOAP Web Services (XML/WS-Security) |
| Current API Version | REST: v1 (stable) / SOAP: per-module versioned WSDLs |
| Editions Covered | All (no edition-gated auth restrictions) |
| Deployment | Cloud (multi-tenant SaaS) |
| API Docs | Workday Community Docs |
| Status | GA |
| API Surface | Protocol | Auth Method | Best For | Real-time? | Bulk? |
|---|---|---|---|---|---|
| REST API | HTTPS/JSON | OAuth 2.0 (Bearer token) | Modern CRUD, mobile/web apps, custom objects | Yes | Limited |
| SOAP Web Services | HTTPS/XML | ISU + WS-Security password or X.509 cert | Complex transactions, payroll, benefits, full HCM object model | Yes | Via batch operations |
| RaaS (Report as a Service) | HTTPS/JSON or XML | OAuth 2.0 or ISU + WS-Security | Custom report extraction, scheduled data pulls | Yes | Yes (report output) |
| Workday Studio | Internal | ISU-linked | Complex multi-step integrations, EIBs, document transforms | Both | Yes |
| Limit Type | Value | Applies To | Notes |
|---|---|---|---|
| Request rate throttle | ~5 req/sec per tenant | REST API (some endpoints) | Soft throttling; no hard published number [src7] |
| Maximum response page size | 100 records (default) | REST API pagination | Use offset and limit parameters [src2] |
| SOAP batch size | 999 records per request | SOAP bulk operations | Split larger payloads [src3] |
| Request body size | ~10 MB | REST API POST/PUT | Large payloads should use EIB [src7] |
| Flow | Use When | Token Lifetime | Refresh? | Notes |
|---|---|---|---|---|
| ISU + Password (SOAP) | SOAP Web Services, legacy integrations | Session-based (timeout = 0 for ISU) | N/A | Do not allow UI sessions [src3] |
| OAuth 2.0 Authorization Code | User-context operations, SSO-initiated flows | Access: 60 min; Refresh: configurable | Yes | Requires redirect URL [src5] |
| OAuth 2.0 JWT Bearer | Server-to-server, automated integrations (recommended) | Access: 60 min | No — new JWT per request | Requires X.509 certificate [src4] |
| OAuth 2.0 Client Credentials | System-to-system, simpler setup | Access: 60 min | Yes | Less secure (secret-based) [src5] |
| X.509 Certificate (SOAP) | Password-free SOAP authentication | Session-based | N/A | Upload public key to Workday [src6] |
An ISU is a dedicated service account for system-to-system integrations. It is the foundation for both SOAP and OAuth flows. [src3]
&, ", >).Eliminates stored secrets by using a signed JWT assertion. [src4]
iss (Client ID), sub (ISU username), aud (token endpoint), exp (max 5 min).grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer.Authorization: Bearer header.Replaces password-based WS-Security with cryptographic verification. [src6]
&, ", > are forbidden; no clear error on violation.START — Authenticate with Workday
├── Which API surface?
│ ├── REST API (JSON, modern)
│ │ ├── Server-to-server (no human in loop)?
│ │ │ ├── YES → OAuth 2.0 JWT Bearer (recommended)
│ │ │ │ ├── Have X.509 certificate? → Proceed with JWT Bearer
│ │ │ │ └── No certificate yet → Generate self-signed for dev, CA-signed for prod
│ │ │ └── Prefer simpler setup? → OAuth 2.0 Client Credentials (less secure)
│ │ └── User-context (SSO, interactive)?
│ │ └── OAuth 2.0 Authorization Code (+PKCE for public clients)
│ ├── SOAP Web Services (XML, legacy/complex)
│ │ ├── Need password-free auth?
│ │ │ ├── YES → X.509 Certificate + WS-Security
│ │ │ └── NO → ISU + Password + WS-Security
│ │ └── Complex multi-step integration?
│ │ └── Workday Studio (uses ISU internally)
│ └── RaaS (Report as a Service)
│ ├── REST endpoint? → OAuth 2.0 (same as REST)
│ └── SOAP endpoint? → ISU or X.509 (same as SOAP)
├── Security requirements?
│ ├── Highest (no stored secrets) → JWT Bearer or X.509
│ ├── Standard (secret in vault) → Client Credentials
│ └── Legacy/testing only → ISU + Password
└── Token management?
├── Long-running batch → Cache access token, refresh before 60 min
├── Frequent short calls → New token per batch, not per call
└── Multi-tenant → Separate API client per tenant
| Auth Method | API Surface | Setup Complexity | Security Level | Password Stored? | Recommended For |
|---|---|---|---|---|---|
| ISU + Password | SOAP only | Low | Medium | Yes (in integration) | Legacy SOAP, quick testing |
| OAuth 2.0 Auth Code | REST | Medium | High | No (token-based) | User-initiated flows, SSO |
| OAuth 2.0 JWT Bearer | REST | High | Highest | No (certificate-based) | Server-to-server production |
| OAuth 2.0 Client Credentials | REST | Medium | High | Client secret in vault | Simpler server-to-server |
| X.509 Certificate | SOAP only | High | Highest | No (certificate-based) | Password-free SOAP, high security |
Every Workday integration starts with an ISU — the service account identity. [src3]
Workday Task: "Create Integration System User"
Fields:
User Name: myapp_isu_prod
Password: <strong random, no &, ", or > chars>
Session Timeout: 0 (never timeout)
Do Not Allow UI: checked
Require New Password: unchecked (for ISU)
Verify: Search "View Integration System User" — confirm "Do Not Allow UI Sessions" = Yes.
ISU has no permissions until assigned to a security group with domain policies. [src3]
Workday Task: "Create Security Group"
Type: Integration System Security Group (Unconstrained)
Name: ISG_MyApp_Prod
Members: myapp_isu_prod
Workday Task: "Edit Domain Security Policy Permissions"
Domain: Worker Data: Public Worker Reports
Security Group: ISG_MyApp_Prod
Permissions: Get (read)
Workday Task: "Activate Pending Security Policy Changes"
Verify: Run "View Security Group Membership for Integration System".
Required for any REST API access. [src5]
Workday Task: "Register API Client for Integrations"
Client Name: MyApp REST Client
Grant Type: Jwt Bearer Grant
X509 Cert: Upload public key
Scope: Staffing, Human Resources, System
ISU: myapp_isu_prod
Record: Client ID + Token Endpoint
Verify: Search "View API Clients" — confirm grant type and linked ISU.
For JWT Bearer flow — public key to Workday, private key in your vault. [src4, src6]
# Generate 2048-bit RSA private key
openssl genrsa -out workday_private.pem 2048
# Generate self-signed X.509 certificate (365 days)
openssl req -new -x509 -key workday_private.pem \
-out workday_cert.pem -days 365 \
-subj "/CN=MyApp Workday Integration/O=MyCompany"
Verify: openssl x509 -in workday_cert.pem -noout -dates
Build and sign a JWT, exchange for access token. [src4]
import jwt, time, requests
CLIENT_ID = "your-client-id-uuid"
ISU_USERNAME = "myapp_isu_prod"
TOKEN_ENDPOINT = "https://wd5-impl-services1.workday.com/ccx/oauth2/your_tenant/token"
with open("workday_private.pem", "r") as f:
private_key = f.read()
now = int(time.time())
signed_jwt = jwt.encode({
"iss": CLIENT_ID, "sub": ISU_USERNAME,
"aud": TOKEN_ENDPOINT,
"iat": now, "exp": now + 300,
}, private_key, algorithm="RS256")
resp = requests.post(TOKEN_ENDPOINT, data={
"grant_type": "urn:ietf:params:oauth:grant-type:jwt-bearer",
"assertion": signed_jwt,
})
access_token = resp.json()["access_token"]
Verify: resp.status_code == 200 and non-empty access_token.
Use the Bearer token to call Workday endpoints. [src2]
curl -s -H "Authorization: Bearer $ACCESS_TOKEN" \
"https://wd5-impl-services1.workday.com/ccx/api/v1/your_tenant/workers?limit=5"
Verify: HTTP 200 with JSON array of worker records.
For SOAP integrations, embed ISU credentials in the WS-Security header. [src3]
<soapenv:Header>
<wsse:Security xmlns:wsse="...">
<wsse:UsernameToken>
<wsse:Username>myapp_isu_prod@your_tenant</wsse:Username>
<wsse:Password Type="...#PasswordText">password</wsse:Password>
</wsse:UsernameToken>
</wsse:Security>
</soapenv:Header>
Verify: SOAP response contains expected data element.
# Input: Workday tenant config (client_id, private_key, isu, endpoint)
# Output: Valid access token or raises error
import jwt, time, requests
from typing import Optional
class WorkdayAuth:
def __init__(self, client_id, isu_username, private_key_path, token_endpoint):
self.client_id = client_id
self.isu_username = isu_username
self.token_endpoint = token_endpoint
with open(private_key_path) as f:
self.private_key = f.read()
self._token: Optional[str] = None
self._token_expiry: float = 0
def get_token(self) -> str:
if self._token and time.time() < self._token_expiry - 120:
return self._token
return self._refresh_token()
def _refresh_token(self, retries=3) -> str:
now = int(time.time())
assertion = jwt.encode({
"iss": self.client_id, "sub": self.isu_username,
"aud": self.token_endpoint, "iat": now, "exp": now + 300,
}, self.private_key, algorithm="RS256")
for attempt in range(retries):
resp = requests.post(self.token_endpoint, data={
"grant_type": "urn:ietf:params:oauth:grant-type:jwt-bearer",
"assertion": assertion,
}, timeout=30)
if resp.status_code == 200:
self._token = resp.json()["access_token"]
self._token_expiry = now + 3600
return self._token
if resp.status_code == 429:
time.sleep(2 ** attempt)
continue
resp.raise_for_status()
raise RuntimeError("Failed to obtain Workday token")
// Input: client_id, client_secret, refresh_token, token_endpoint
// Output: { access_token, refresh_token } object
import fetch from 'node-fetch'; // [email protected]
async function getWorkdayAccessToken({ clientId, clientSecret, refreshToken, tokenEndpoint }) {
const credentials = Buffer.from(`${clientId}:${clientSecret}`).toString('base64');
const resp = await fetch(tokenEndpoint, {
method: 'POST',
headers: {
'Authorization': `Basic ${credentials}`,
'Content-Type': 'application/x-www-form-urlencoded',
},
body: new URLSearchParams({
grant_type: 'refresh_token',
refresh_token: refreshToken,
}),
});
if (!resp.ok) throw new Error(`Workday auth failed: ${resp.status}`);
return resp.json(); // { access_token, token_type, refresh_token }
}
# Input: Client ID, Client Secret, Refresh Token
# Output: JSON { access_token, token_type, refresh_token }
curl -X POST "https://wd5-impl-services1.workday.com/ccx/oauth2/your_tenant/token" \
-u "your_client_id:your_client_secret" \
-d "grant_type=refresh_token&refresh_token=your_refresh_token" \
-H "Content-Type: application/x-www-form-urlencoded"
| Credential | Where Stored | Where Used | Rotation Policy | Gotcha |
|---|---|---|---|---|
| ISU Username | Workday tenant | SOAP WS-Security, OAuth sub claim | Rarely changes | Must include @tenant for SOAP |
| ISU Password | Integration vault | SOAP WS-Security only | Per org policy; ISU exempt | No &, ", > chars |
| Client ID | Workday API Client | JWT iss claim, Basic Auth | Does not change | UUID, auto-generated |
| Client Secret | Integration vault | Basic Auth (non-JWT flows) | Rotate regularly | Not used with JWT Bearer |
| X.509 Public Key | Workday tenant | Linked to API client or ISU | Renew before expiry | Must re-upload on renewal |
| X.509 Private Key | Integration vault (NEVER Workday) | JWT signing, WS-Security signing | Matches public key | Loss = re-register in Workday |
| Refresh Token | Integration vault | Token endpoint (refresh grant) | Rotates on each use | New token per refresh |
| Access Token | In-memory only | Authorization: Bearer header | 60-min auto-expiry | Do not persist to disk |
| Code | Meaning | Cause | Resolution |
|---|---|---|---|
| 401 | Unauthorized | Wrong ISU password, expired token, missing Bearer | Re-authenticate; check credentials |
| 403 | Forbidden | ISU security group lacks domain access | Add permissions, activate policy changes |
| 429 | Too Many Requests | Exceeded throttle or session limit | Exponential backoff (2^n sec, max 5 retries) |
| 400 | Bad Request | Malformed token request, expired JWT | Verify JWT claims (iss, sub, aud, exp) |
| 500 | Internal Server Error | Transient platform error | Retry with backoff; check status page |
| SOAP Fault | SignatureVerificationFailed | Certificate mismatch or corrupted message | Re-verify cert pairing |
Set alerts 30 days before cert expiry; automate renewal. [src6]Persist new refresh token atomically before using access token. [src2]Separate ISUs per integration; check linked clients before deactivation. [src3]Always run "Activate Pending Security Policy Changes". [src3]Test in sandbox during 4-6 week preview window. [src7]Parameterize tenant in all endpoint URLs. [src2]# BAD — ISU password cannot authenticate REST API; returns 401
resp = requests.get(url, auth=("isu@tenant", "password"))
# GOOD — Token-based, no stored password, 60-min expiry
resp = requests.get(url, headers={"Authorization": f"Bearer {token}"})
# BAD — Audit log shows same user for everything
ISU: shared_user → Payroll + Benefits + Recruiting + Reports
# GOOD — Clear audit trail, isolated failure domain
ISU: payroll_isu → Payroll only
ISU: benefits_isu → Benefits only
ISU: recruiting_isu → Recruiting only
# BAD — Crash between refresh and persist loses token chain
new_tokens = refresh(old_token)
do_work(new_tokens["access_token"]) # might crash
save(new_tokens["refresh_token"]) # never reached
# GOOD — Atomic rotation: persist first, use second
new_tokens = refresh(old_token)
save(new_tokens["refresh_token"]) # persist FIRST
do_work(new_tokens["access_token"]) # safe to fail
username@tenant — omitting returns ambiguous error. Fix: Always append @tenant_name in SOAP headers. [src3]Set 90-day expiry with automated re-authorization. [src5]Always run activation task after permission edits. [src3]Separate certificates per environment. [src6]Store tenant as environment variable. [src2]Test in sandbox during preview window. [src7]# Test OAuth 2.0 token endpoint connectivity
curl -v -X POST "https://wd5-impl-services1.workday.com/ccx/oauth2/your_tenant/token" \
-d "grant_type=refresh_token&refresh_token=test" \
-u "client_id:client_secret" 2>&1 | head -30
# Verify X.509 certificate details and expiration
openssl x509 -in workday_cert.pem -noout -subject -dates -issuer
# Check certificate and private key match
openssl x509 -in workday_cert.pem -noout -modulus | openssl md5
openssl rsa -in workday_private.pem -noout -modulus | openssl md5
# Verify REST API access with Bearer token
curl -s -o /dev/null -w "%{http_code}" \
-H "Authorization: Bearer your_access_token" \
"https://wd5-impl-services1.workday.com/ccx/api/v1/your_tenant/workers?limit=1"
# Generate new self-signed X.509 certificate
openssl req -new -x509 -key workday_private.pem -out workday_cert_new.pem \
-days 365 -subj "/CN=MyApp Workday/O=MyCompany"
| Release | Date | Status | Auth Changes | Migration Notes |
|---|---|---|---|---|
| 2025R2 | 2025-09 | Current | OAuth scope refinements | Review scope assignments |
| 2025R1 | 2025-03 | Supported | Minor WSDL updates | No breaking auth changes |
| 2024R2 | 2024-09 | Supported | OAuth 2.0 scope model updates | May require API client re-registration |
| 2024R1 | 2024-03 | EOL (approaching) | PKCE support added | Optional — existing flows unaffected |
Workday supports SOAP API versions for approximately 2-3 years. REST API v1 is stable and not versioned the same way — breaking changes are rare and announced via Workday Community. Always test in sandbox during the 4-6 week preview window before each R1/R2 rollout. [src7]
| Use When | Don't Use When | Use Instead |
|---|---|---|
| Automated server-to-server integration with Workday | One-time manual data extract | Workday Report Writer or CSV export |
| Custom integration reading/writing worker or financial data | Using pre-built iPaaS connector (Workato, MuleSoft) | iPaaS handles auth internally |
| Password-free auth for compliance (SOC 2, ISO 27001) | Testing or prototyping only | ISU + Password for quick testing |
| Multi-tenant SaaS connecting to customer Workday tenants | Single-use script that runs once | Simple ISU + Password |
| Fine-grained audit trail per integration | Integration within Workday Studio | Studio manages its own ISU context |