TL;DR
POST creates new resources, PUT replaces entire resources, PATCH updates specific fields. For credits, always use batch: POST /credits/batch with {"value": [...array...]}, not individual POSTs. Each credit requires: participantId, transactionDate, amount, currencyCode, periodId, sourceRef (idempotency key).
The batch response has {"results": [...]}, each with sourceRef, status (CREATED/ERROR), and errorMessage. Parse it: separate successes from errors, log errors for investigation, retry if needed. Use sourceRef to link response back to your source transaction.
For quotas, use PUT /quotas/{quotaId} with full object or PATCH for partial updates. Always GET first to fetch current state, then modify. Idempotency: sourceRef on credits prevents duplicates if you POST the same sourceRef twice (409 Conflict = already exists, safe to ignore). Pipeline triggers: POST /pipeline-runs, poll GET /pipeline-runs/{runId} for status.

HTTP Methods for Writing Data

MethodMeaningICM Use CaseIdempotent?
POSTCreate new resourcePost credits, trigger pipeline runsNo — each POST creates a new record (unless sourceRef prevents duplicate)
PUTReplace entire resourceUpdate quota allocations (full replacement)Yes — replacing the same data multiple times is safe
PATCHPartial update (change specific fields)Update participant status, change one quota fieldSometimes — depends on implementation
DELETERemove resourceRarely used in ICM (no delete endpoints for credits)Yes — deleting again is safe

Posting Credits: The Most Common Write Operation

Posting credit transactions from your CRM to SAP SuccessFactors IM is the bread and butter of ICM integrations. Always batch them.

Why Batch? Why Not Individual POSTs?

Posting 10,000 credits one at a time means 10,000 HTTP requests, each with TLS handshake overhead. It's slow, wasteful, and triggers rate limits. Batch them instead: send 100 credits per request, finish in 100 requests.

Credit Payload Structure

Each credit transaction must include these fields:

FieldTypeExampleNotes
participantIdString"P001234"REQUIRED. Must exist in IM.
transactionDateDate (ISO 8601)"2026-04-12"REQUIRED. When the sale/commission occurred.
amountDecimal4500.00REQUIRED. Credit amount in the specified currency.
currencyCodeString"USD"REQUIRED. Three-letter code (USD, EUR, etc.)
periodIdString"Q1-2026"REQUIRED. Comp period for this credit.
sourceRefString"CRM-ORDER-98765"REQUIRED. Unique ID from your source system. Prevents duplicate posting.
descriptionString"Order #98765 from Acme"Optional. Visible in IM UI.

Batch POST Request

JSON — Batch credit POST body
{
  "value": [
    {
      "participantId": "P001234",
      "transactionDate": "2026-04-12",
      "amount": 4500.00,
      "currencyCode": "USD",
      "periodId": "Q1-2026",
      "sourceRef": "CRM-ORDER-98765",
      "description": "Order from Acme Corp"
    },
    {
      "participantId": "P001235",
      "transactionDate": "2026-04-12",
      "amount": 2250.00,
      "currencyCode": "USD",
      "periodId": "Q1-2026",
      "sourceRef": "CRM-ORDER-98766"
    }
  ]
}

Batch Response and Processing

JSON — Batch credit response
{
  "results": [
    {
      "sourceRef": "CRM-ORDER-98765",
      "status": "CREATED",
      "creditId": "CRED-001"
    },
    {
      "sourceRef": "CRM-ORDER-98766",
      "status": "ERROR",
      "errorMessage": "Participant P001235 not found in this period"
    }
  ]
}

Each result maps back to the request via sourceRef. Process by separating successes from errors:

Python — Process batch response
import requests

def post_credits_batch(token, credits, batch_size=100):
    """Post credits in batches, handle responses."""
    url = "https://api.sap.com/successfactors/im/credits/batch"
    headers = {
        "Authorization": f"Bearer {token}",
        "Content-Type": "application/json"
    }

    all_successes = []
    all_errors = []

    # Process in batches of 100
    for i in range(0, len(credits), batch_size):
        batch = credits[i:i+batch_size]
        payload = {"value": batch}

        resp = requests.post(url, json=payload, headers=headers)
        resp.raise_for_status()

        data = resp.json()
        results = data.get("results", [])

        # Separate by status
        successes = [r for r in results if r["status"] == "CREATED"]
        errors = [r for r in results if r["status"] == "ERROR"]

        all_successes.extend(successes)
        all_errors.extend(errors)

        print(f"Batch {i//batch_size + 1}: {len(successes)} created, {len(errors)} errors")

    return all_successes, all_errors

  

Updating Quotas: PUT vs PATCH

Quota updates come from planning tools. Two approaches: replace the entire quota (PUT) or update specific fields (PATCH).

PUT: Full Replacement

Use PUT to replace a quota entirely:

Python — Update quota with PUT
# Step 1: GET current quota
GET /quotas/QUOTA-4567

# Step 2: Build new quota object (must be complete)
new_quota = {
    "id": "QUOTA-4567",
    "participantId": "P001234",
    "periodId": "Q1-2026",
    "quotaAmount": 100000.00,  # Updated
    "currency": "USD",
    "status": "ACTIVE"
}

# Step 3: PUT the full object
PUT /quotas/QUOTA-4567

PATCH: Partial Update

Use PATCH to update only specific fields:

Python — Update quota with PATCH
# Only update quotaAmount, leave other fields unchanged PATCH /quotas/QUOTA-4567 { "quotaAmount": 100000.00 }

Idempotency: The sourceRef Pattern

Idempotency is crucial for production integrations. If your credit push job crashes and you rerun it, you need to post the same 10,000 credits again. Without idempotency, you'd create duplicates. With sourceRef, the second POST with the same sourceRef returns 409 Conflict (already exists) — safe to ignore.

sourceRef is your integration's unique ID for the transaction. Set it to a stable, non-changing ID from your source system (CRM order ID, ERP posting ID, etc.)

Triggering Pipeline Runs

After posting credits, you typically trigger a calculation pipeline run. POST to /pipeline-runs:

Python — Trigger pipeline run
import requests
import time

url = "https://api.sap.com/successfactors/im/pipeline-runs"
headers = {
    "Authorization": f"Bearer {token}",
    "Content-Type": "application/json"
}

# Trigger the run
payload = {
    "pipelineId": "PIPELINE-Q1-2026",
    "periodId": "Q1-2026"
}

resp = requests.post(url, json=payload, headers=headers)
data = resp.json()
run_id = data["id"]

print(f"Pipeline run started: {run_id}")

# Poll for completion
while True:
    resp = requests.get(f"https://api.sap.com/successfactors/im/pipeline-runs/{run_id}", headers=headers)
    run = resp.json()
    status = run["status"]

    if status == "COMPLETED":
        print(f"Run completed successfully")
        break
    elif status == "FAILED":
        print(f"Run failed: {run.get('errorMessage', 'Unknown error')}")
        break
    else:
        print(f"Status: {status}, waiting...")
        time.sleep(10)  # Poll every 10 seconds

Batch Size and Performance

How large should your batches be? The tradeoff is between request overhead (large batches) and memory usage (very large batches):

  • 100 records per batch: Standard safe size. Most APIs support up to 200, but 100 is conservative.
  • Smaller batches (50): If your records are large or network is unreliable, smaller batches reduce risk of timeout.
  • Larger batches (200): Only if you've tested and confirmed your API instance supports it.

A nightly credit push with 10,000 records at 100 per batch = 100 POST requests. At ~500ms per request, that's 50 seconds total. Reasonable and performant.

Handling 409 Conflict: Idempotency in Action

The 409 Conflict response is the most important response for production integrations. It means "the record you're trying to create already exists." This is your idempotency guarantee:

  • First run: POST 10,000 credits with sourceRef = CRM order IDs. All succeed (200/201 responses).
  • Job crashes.
  • Second run: POST same 10,000 credits with same sourceRef. All return 409 (already exists). Safe to log and move on.
  • Result: No duplicates created. Your job is resilient to failures.

This is why sourceRef must be stable and unique in your source system — never use a UUID or timestamp that changes on each run.

Error Handling: Common Write Errors

Status CodeMeaningAction
400 Bad RequestMalformed request. Missing required field, or invalid format.Check the errorMessage in response. Fix the payload. Do NOT retry.
401 UnauthorizedToken expired or invalid.Fetch a new token, retry the request.
404 Not FoundResource doesn't exist (participant, quota, pipeline).Validate IDs before posting. Do NOT retry.
409 ConflictDuplicate sourceRef (credit already posted).Safe to ignore (idempotent). Already exists in IM.
422 Unprocessable EntityParticipant found but not eligible (wrong period, status, etc.)Investigate the specific error. Do NOT retry without fixing.
429 Too Many RequestsRate limit exceeded.Back off. Use exponential backoff (1s, 2s, 4s, 8s). Retry.
500 Server ErrorAPI server-side problem.Transient. Use exponential backoff. Retry.

Writing Data at Scale: Best Practices

When you move from testing to production, follow these patterns:

  • Always batch. Never POST one record at a time. Batch 100-200 per request.
  • Always set sourceRef. Use a stable ID from your source system for idempotency.
  • Always parse the response. Separate successes from errors. Log errors with their sourceRef so you can investigate.
  • Handle 409 gracefully. It's not an error — it's an idempotent success. The record already exists.
  • Retry only retriable errors. 4xx (except 429) are permanent — fix and resend. 5xx/429 are transient — exponential backoff and retry.
  • Log everything. Job ID, batch number, success count, error count, each failed sourceRef. Make it easy to debug later.

curl Examples for Manual Testing

Shell — POST single credit (for testing)
# Post one credit via batch endpoint curl -X POST \ -H "Authorization: Bearer YOUR_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "value": [ { "participantId": "P001234", "transactionDate": "2026-04-12", "amount": 4500.00, "currencyCode": "USD", "periodId": "Q2-2026", "sourceRef": "CRM-ORDER-98765" } ] }' \ "https://api.sap.com/successfactors/im/credits/batch" \ | python3 -m json.tool

Choosing Between POST, PUT, and PATCH

When should you use each method?

  • POST /credits/batch: Create new credit transactions. Always use batch. The most common write operation in ICM integrations.
  • PUT /quotas/{id}: Replace an entire quota object. You must fetch the current object first (GET), modify it completely, and send it back. Use this when updating quotas from a planning tool.
  • PATCH /quotas/{id}: Update specific fields only. Send only the fields you're changing. Less error-prone than PUT because you don't need to refetch the full object first.
  • POST /pipeline-runs: Trigger a calculation run. Not "creating" a run in the traditional sense — it's a fire-and-forget command that kicks off a background job.

For quota updates in production, prefer PATCH over PUT — it's safer and reduces the risk of overwriting fields you didn't intend to change.

Testing Your Write Integration

Before going live, test these scenarios:

  1. Successful batch: POST 10 valid credits. Verify all return CREATED status.
  2. Duplicate detection: POST the same 10 credits again (same sourceRef). Verify all return 409 Conflict. Verify no duplicates in IM.
  3. Mixed batch: POST 5 valid + 5 invalid (wrong participant ID). Verify 5 succeed, 5 fail with appropriate error messages.
  4. Field validation: Intentionally omit required fields. Verify 400 Bad Request with clear error message telling which field is missing.
  5. Token expiry: If your test takes > 30 minutes, verify token refresh works (401 handling).

These tests give you confidence that your integration handles the full range of success and error scenarios correctly

You now understand how to write data: batch credits, update quotas, and trigger pipelines. But production integrations need more: rate limit handling, retry logic, monitoring, and error resilience. Move to Lesson 5: Production Patterns.