What You Can Query
Every major resource in SAP SuccessFactors IM can be fetched with GET requests:
| Resource | GET Path | Common Filters |
|---|---|---|
| Participants | GET /participants | status, region, managerId, effectiveDate |
| Comp Plans | GET /comp-plans | status, planType |
| Results | GET /results | participantId, periodId, status, payoutAmountGE, payoutAmountLE |
| Quotas | GET /quotas | participantId, periodId, status |
| Credits | GET /credits | participantId, periodId, status, transactionDateGE, transactionDateLE |
| Pipeline Runs | GET /pipeline-runs | pipelineId, status, createdDateGE |
OData Query Parameters: The Core Tools
$filter: Select Records Conditionally
The $filter parameter lets you exclude records you don't need. Instead of fetching all 50,000 participants and filtering in code, push the filter to the server:
// Single condition GET /participants?$filter=status eq 'ACTIVE' // Not equals GET /participants?$filter=status ne 'INACTIVE' // Greater than, less than GET /results?$filter=payoutAmount gt 10000 GET /credits?$filter=transactionDate lt '2026-04-01' // AND condition GET /results?$filter=status eq 'APPROVED' and periodId eq 'Q1-2026' // OR condition GET /participants?$filter=status eq 'ACTIVE' or status eq 'INACTIVE' // Parentheses for grouping GET /results?$filter=(status eq 'APPROVED' or status eq 'PENDING') and periodId eq 'Q1-2026'
Key syntax rules:
- String values in single quotes: status eq 'ACTIVE' (not "ACTIVE" or unquoted)
- Dates in ISO 8601 format: '2026-04-12' or '2026-04-12T15:30:00Z'
- Numbers unquoted: payoutAmount gt 10000
- Case-sensitive field names: transactionDate, not TransactionDate
- Case-sensitive operators: 'eq', 'ne', 'gt', 'lt', 'ge' (greater-or-equal), 'le' (less-or-equal)
$select: Fetch Only the Fields You Need
By default, GET /participants returns all 50+ fields per record. If you only need id, name, and status, use $select to reduce payload:
// Fetch only these three fields GET /participants?$select=id,name,status // With filter and select GET /participants?$filter=status eq 'ACTIVE'&$select=id,name,region // Comma-separated, no spaces (or spaces are URL-encoded as %20) GET /results?$select=id,participantId,payoutAmount,periodId,status
Benefits:
- Smaller response payload: 5 fields vs 50+ fields = 10x smaller JSON
- Faster API response: Less data to serialize and transmit
- Faster client processing: Less data to parse and store in memory
Always use $select in production integrations. The only time you fetch all fields is when you're exploring the API and don't know what you need yet.
$orderby: Sort Results
When you paginate, always sort. Sorting ensures consistent ordering so you don't get duplicate or missing records when following pagination links:
// Ascending order (default) GET /participants?$orderby=id // Descending GET /results?$orderby=createdDate desc // Multiple fields GET /credits?$orderby=participantId,transactionDate desc // With filter, select, and orderby GET /results?$filter=status eq 'APPROVED'&$select=id,participantId,payoutAmount&$orderby=id
$top: Page Size
Set the maximum number of records per page. SAP SuccessFactors IM typically allows 100-200 records per page:
// Fetch 100 records per page GET /participants?$top=100 // With other parameters GET /results?$filter=periodId eq 'Q1-2026'&$top=100&$orderby=id
$skip: Offset Pagination (Avoid This)
OData supports $skip to skip N records, but SAP SuccessFactors IM doesn't recommend it. Use the $skiptoken in @odata.nextLink instead. $skip is inefficient for large datasets.
Response Envelope and Pagination
A GET /participants response looks like:
{
"@odata.context": "https://api.sap.com/successfactors/im/$metadata#Participants",
"@odata.count": 12500,
"value": [
{ "id": "P001234", "name": "Alice Johnson", "status": "ACTIVE" },
{ "id": "P001235", "name": "Bob Smith", "status": "ACTIVE" },
...98 more records...
],
"@odata.nextLink": "https://api.sap.com/successfactors/im/participants?$top=100&$skiptoken=abc123xyz789"
}Key fields:
- @odata.context: Metadata URL (mostly for documentation)
- @odata.count: Total number of records matching the filter (12,500 participants total)
- value: The actual array of records. This is where your data is. Not at the root level.
- @odata.nextLink: If present, there are more pages. Follow this URL (exactly as-is) to get the next page. Don't modify it, don't extract the skiptoken and rebuild the URL. Just follow the link.
If @odata.nextLink is absent, you've reached the last page.
Pagination Loop Pattern
Here's the correct way to fetch all pages:
import requests def fetch_all_participants(token): """Fetch all active participants, paginating through all results.""" base_url = "https://api.sap.com/successfactors/im/participants" headers = {"Authorization": f"Bearer {token}"} # First request with all parameters params = { "$filter": "status eq 'ACTIVE'", "$select": "id,name,status,region", "$orderby": "id", "$top": 100 } all_records = [] url = base_url while url: # Fetch this page resp = requests.get(url, headers=headers, params=params) resp.raise_for_status() data = resp.json() # Extract records from 'value' array records = data.get("value", []) all_records.extend(records) print(f"Fetched {len(records)} records, total so far: {len(all_records)}") # Check for next page url = data.get("@odata.nextLink") # Will be None if no more pages params = {} # Only pass params on first request; nextLink includes them print(f"Done. Fetched {len(all_records)} total active participants") return all_records # Usage: participants = fetch_all_participants(token)
Real-World Examples
Example 1: Get All Approved Results for a Period
def get_approved_results(token, period_id): """Fetch all APPROVED results for a comp period (for payroll export).""" url = "https://api.sap.com/successfactors/im/results" headers = {"Authorization": f"Bearer {token}"} params = { "$filter": f"periodId eq '{period_id}' and status eq 'APPROVED'", "$select": "id,participantId,payoutAmount,currency,payoutDate", "$orderby": "participantId", "$top": 200 } all_results = [] url_to_fetch = url while url_to_fetch: resp = requests.get(url_to_fetch, headers=headers, params=params) resp.raise_for_status() data = resp.json() all_results.extend(data.get("value", [])) url_to_fetch = data.get("@odata.nextLink") params = {} return all_results
Example 2: Get a Single Participant by ID
def get_participant_by_id(token, participant_id): """Get a single participant (no pagination needed).""" url = f"https://api.sap.com/successfactors/im/participants/{participant_id}" headers = {"Authorization": f"Bearer {token}"} resp = requests.get(url, headers=headers) resp.raise_for_status() # Single resource response is the object itself, not wrapped in 'value' participant = resp.json() return participant
Performance Optimization Tips
Use $select Aggressively
Fetching 50 fields when you need 5 is wasteful. Always use $select in production:
// Without $select: returns 50+ fields per record GET /participants?$filter=status eq 'ACTIVE'&$top=100 // Response size: ~500KB (50 fields × 100 records × 100 bytes per field) // With $select: returns only needed fields GET /participants?$filter=status eq 'ACTIVE'&$select=id,name&$top=100 // Response size: ~10KB (2 fields × 100 records) // 50x smaller, 50x faster to parse
Common Mistakes
Mistake 1: Assuming First Page Is All Data
Never assume @odata.nextLink is absent. Always loop and follow the link:
# WRONG: assumes first page is all data resp = requests.get("https://api.sap.com/successfactors/im/participants", headers=headers) participants = resp.json()["value"] # RIGHT: loop through all pages while url:
Mistake 2: Not Using $select
Fetching all fields is slow. Always use $select in production to reduce payload by 50-90%.
Mistake 3: Not Sorting When Paginating
Without $orderby, results may be in inconsistent order. You could get duplicate records or miss some when paginating. Always add $orderby to paginated requests.
OData Query Parameter Reference
| Parameter | Purpose | Example | Notes |
|---|---|---|---|
| $filter | Filter records by condition(s) | status eq 'ACTIVE' | Use eq, ne, gt, lt, ge, le; and/or for combining; parentheses for grouping |
| $select | Choose which fields to return | id,name,status | Comma-separated. Omitting returns all fields (slower). |
| $orderby | Sort results | id asc or createdDate desc | Essential for pagination. Controls sort order. |
| $top | Page size (max records per page) | 100 | Max typically 100-200. SAP enforces a limit. |
| $skip | Offset pagination (NOT recommended) | 100 | Inefficient. Use @odata.nextLink instead. |
Real curl Examples
# Get APAC participants with minimal fields curl -X GET \ -H "Authorization: Bearer YOUR_TOKEN" \ "https://api.sap.com/successfactors/im/participants?\ $filter=status eq 'ACTIVE' and region eq 'APAC'\ &$select=id,name,position\ &$orderby=id\ &$top=100" # Get approved results for a period, sorted by payout amount curl -X GET \ -H "Authorization: Bearer YOUR_TOKEN" \ "https://api.sap.com/successfactors/im/results?\ $filter=periodId eq 'Q2-2026' and status eq 'APPROVED'\ &$select=id,participantId,payoutAmount\ &$orderby=payoutAmount desc\ &$top=100"
Troubleshooting Common Query Issues
No Results Returned
Check the $filter syntax. Common mistakes:
- Wrong case: status eq 'active' (lowercase) instead of 'ACTIVE' (uppercase)
- Wrong operators: status = 'ACTIVE' instead of eq
- Missing quotes: status eq ACTIVE instead of 'ACTIVE'
- Date format: Use ISO 8601 format only: '2026-04-12', not '04/12/2026'
Partial Results (Missing Records)
Likely causes:
- You didn't loop through all pages. Check if @odata.nextLink is present and follow it.
- No $orderby specified. Results are in inconsistent order; follow pagination strictly.
- $top is too small or too large. Use 100 as a safe default.
Slow Query
Optimize:
- Add more specific $filter conditions (e.g., also filter by period, region, or date range).
- Always use $select to reduce fields returned.
- Increase $top (if API allows, up to 200) to reduce number of requests.
Performance Checklist
- Use $select: Always. Never fetch all fields.
- Use $filter: Push filtering to the server, not to your code.
- Use $orderby: Essential for pagination. Ensures consistent ordering.
- Follow @odata.nextLink: Never assume first page is all data. Always loop.
- Never manually build $skiptoken: It's opaque. Always use the nextLink URL as-is.
You now know how to read data: filter with $filter, select fields with $select, sort with $orderby, and paginate with @odata.nextLink. In Lesson 4, you'll learn how to write data: POST for new records, PUT/PATCH for updates, and the critical batch pattern for production scale.