TL;DR
SAP SuccessFactors IM uses OData-style query parameters for GETs: $filter (equals, not equals, greater-than, less-than, and/or, parentheses), $select (choose fields), $orderby (sort), $top (page size, max ~100-200), $skip (offset). Build filters like: status eq 'ACTIVE', periodId eq 'Q1-2026', status eq 'APPROVED' and periodId eq 'Q1-2026'.
The response envelope contains @odata.nextLink when more pages exist. Always follow the nextLink — never manually build skip tokens or assume the first page is all data. Build a pagination loop: fetch page, process records, follow nextLink, repeat until no nextLink remains.
Use $select to fetch only the fields you need — reduces payload size, faster response times. Always $orderby when paginating (avoid duplicate/missing records). Always handle the response envelope properly: data is in the "value" array, not at the root level.

What You Can Query

Every major resource in SAP SuccessFactors IM can be fetched with GET requests:

ResourceGET PathCommon Filters
ParticipantsGET /participantsstatus, region, managerId, effectiveDate
Comp PlansGET /comp-plansstatus, planType
ResultsGET /resultsparticipantId, periodId, status, payoutAmountGE, payoutAmountLE
QuotasGET /quotasparticipantId, periodId, status
CreditsGET /creditsparticipantId, periodId, status, transactionDateGE, transactionDateLE
Pipeline RunsGET /pipeline-runspipelineId, 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:

OData Filter Syntax
// 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:

OData $select
// 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:

OData $orderby
// 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:

OData $top
// 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:

JSON — Paginated GET response
{
  "@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:

Python — Pagination loop
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

Python — Fetch approved results
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

Python — Single resource GET
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:

Comparison: With vs Without $select
// 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:

Python — WRONG vs RIGHT
# 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.

⚠️Pagination is mandatory: Assume every GET request returns a paginated response. Always check for @odata.nextLink and loop until it's absent. Your 50,000 participants won't come back in one request.

OData Query Parameter Reference

ParameterPurposeExampleNotes
$filterFilter records by condition(s)status eq 'ACTIVE'Use eq, ne, gt, lt, ge, le; and/or for combining; parentheses for grouping
$selectChoose which fields to returnid,name,statusComma-separated. Omitting returns all fields (slower).
$orderbySort resultsid asc or createdDate descEssential for pagination. Controls sort order.
$topPage size (max records per page)100Max typically 100-200. SAP enforces a limit.
$skipOffset pagination (NOT recommended)100Inefficient. Use @odata.nextLink instead.

Real curl Examples

Shell — Get active APAC participants
# 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.