A minimal Express-based webhook microservice for SAP Sales and Service Cloud V2 that demonstrates both synchronous and asynchronous integration patterns. This service receives webhooks from SAP Sales and Service Cloud V2, calculates custom scores based on ABC classification, and updates accounts accordingly.
This is a very simplified example. You could achieve the same result also with the in-app no-code tools of the in-build customization (a simple if this than that) but I want with this example to show the different nature of the sync and async options you have when writing custom logic. Also I want to convince you that this is a very powerful option to do custom code with external microservices. Your business logic inside those "cloud functions" can then get as complicated as you want ;)
Personaly I would recommend to build this central extension microservice where you can place all your endpoints for custom logic and maintain all your custom code in a central place.
- β Synchronous Webhook - Immediate response with calculated score, returns full payload
- β Asynchronous Webhook - Background processing with 10-second delay and CRM API update
- β CloudEvents Support - Handles CloudEvents format with data wrapper
- β ABC Classification Logic - Auto-scoring (A=90, B=70, C=50)
- β CORS Enabled - Cross-origin requests supported
- β Clean Logging - Operational logs without verbose payload dumps
- β Secure API Integration - Server-side Basic Auth for CRM API calls
- β Optimistic Locking - ETag-based concurrency control for updates
- β Minimal Dependencies - Express + dotenv only
- β Cloud Foundry Ready - Production deployment configuration included
- β Lightweight - ~250 lines of code, 128MB memory footprint
- Node.js 22.x or higher
- SAP Sales and Service Cloud V2 access with API credentials
-
Clone the repository
git clone <repository-url> cd crm-webhook-service
-
Install dependencies
npm install
-
Configure environment
copy env-template.txt .env
Edit
.envwith your SAP CRM credentials:CRM_BASE_URL=https://your-tenant.crm.cloud.sap CRM_USERNAME=your-username CRM_PASSWORD=your-password PORT=3000
-
Start the service
npm run dev
Service runs on
http://localhost:3000 -
Test the webhooks
Synchronous webhook:
curl -X POST http://localhost:3000/webhooks/calculate-score-sync \
-H "Content-Type: application/json" \
-d @webhook-payload.jsonAsynchronous webhook:
curl -X POST http://localhost:3000/webhooks/calculate-score-async \
-H "Content-Type: application/json" \
-d @webhook-payload.jsonEndpoint: POST /webhooks/calculate-score-sync
Purpose: Calculate and return CustomScore immediately based on ABC classification. Use this when CRM needs the result before completing the save operation.
Request Payload (CloudEvents format):
{
"id": "ec7e11d8-ed35-449b-affe-bccbc58b1412",
"specversion": "0.2",
"type": "sap.crm.custom.event.updateAccount",
"source": "63d2b99cd28c19121118a781",
"subject": "0197a1ea-0afb-711a-9b74-03460a24dff7",
"time": "2026-01-13T13:59:32.934399888Z",
"datacontenttype": "application/json",
"data": {
"beforeImage": {
"id": "0197a1ea-0afb-711a-9b74-03460a24dff7",
"displayId": "2136299",
"customerABCClassification": "B",
"isProspect": false,
"extensions": {
"CustomScore": 47
}
},
"currentImage": {
"id": "0197a1ea-0afb-711a-9b74-03460a24dff7",
"displayId": "2136299",
"customerABCClassification": "A",
"isProspect": false,
"adminData": {
"updatedOn": "2026-01-13T13:59:32.671Z"
},
"extensions": {
"CustomScore": 63
}
},
"dataContext": {
"requestedByUser": "b8adfb7e-50f3-11f0-a28d-fd9b4de46c56",
"requestProcessedOn": "2026-01-13T13:59:32.671Z"
}
}
}Response: (200 OK)
{
"data": {
"id": "0197a1ea-0afb-711a-9b74-03460a24dff7",
"displayId": "2136299",
"customerABCClassification": "A",
"isProspect": false,
"adminData": {
"updatedOn": "2026-01-13T13:59:32.671Z"
},
"extensions": {
"CustomScore": 90
}
}
}Note: The response returns the complete currentImage with only the CustomScore field updated. All other fields from the request are preserved.
Error Response: (400 Bad Request)
{
"error": "Missing currentImage object"
}Endpoint: POST /webhooks/calculate-score-async
Purpose: Accept webhook, respond immediately, then process in background with a simulated 10-second delay and update CRM via API. Applies a 15-point penalty for prospects.
Request Payload: Same CloudEvents format as synchronous webhook
Immediate Response: (202 Accepted)
{
"accepted": true,
"message": "Processing in background"
}Background Processing:
- Waits 10 seconds (simulated processing delay)
- Calculates CustomScore based on ABC classification
- Fetches current account from CRM to get fresh ETag
- Updates account via PATCH with calculated score
- Logs success or failure (no callback to CRM on error)
The service automatically calculates CustomScore based on customerABCClassification:
| ABC Class | Base Score | Prospect Penalty | Final Score (Prospect) | Used In |
| ----------- | -Score | Description |
|---|---|---|
| A | 90 | Top-tier customers |
| B | 70 | Mid-tier customers |
| C | 50 | Standard customers |
| Invalid/Missing | 50 | Defaults to C classification |
Notes:
- Same scoring logic applies to both sync and async webhooksscore 50)
SAP CRM sends webhooks in CloudEvents format with data wrapped in a data object:
{
"id": "unique-event-id",
"specversion": "0.2",
"type": "sap.crm.custom.event.updateAccount",
"source": "tenant-id",
"subject": "account-id",
"time": "2026-01-13T13:59:32.934399888Z",
"datacontenttype": "application/json",
"data": {
"beforeImage": {...},
"currentImage": {...},
"dataContext": {...}
}
}The service supports both CloudEvents format (with data wrapper) and direct format (without wrapper) for flexibility.
data.currentImage.id- Account UUID (required)data.currentImage.displayId- Human-readable account ID (for logging)data.currentImage.customerABCClassification- ABC classdata.currentImage.customerABCClassification- ABC class code (A/B/C)data.currentImage.extensions.CustomScore- Current score valuedata.currentImage.adminData.updatedOn- Used for ETag generation
β Required fields:
data.currentImageobject (orcurrentImageif no wrapper)data.currentImage.id(account UUID)
data.beforeImage(not required, but included in CloudEvents)customerABCClassification(defaults to "C"
Async webhook calls:
GET /sap/c4c/api/v1/account-service/accounts/{id}- Fetch account for ETagPATCH /sap/c4c/api/v1/account-service/accounts/{id}- Update CustomScore
- Method: HTTP Basic Authentication
- Header:
Authorization: Basic <base64-encoded-credentials> - Credentials: Loaded from environment variables
The async webhook uses If-Match header with ETag to prevent conflicts:
- Fetches account to get current ETag from response headers
- Includes ETag in PATCH request via
If-Matchheader - CRM rejects update if account was modified since fetch (409 Conflict)
The service includes comprehensive logging for testing and debugging:
### Operational Logsβ Calculated score: 70 (ABC: B) π Starting async processing for account 0197a1ea-0afb-711a-9b74-03460a24dff7... β³ Simulating 10-second processing delay... π‘ Fetching current account data for 0197a1ea-0afb-711a-9b74-03460a24dff7... β Received ETag: "2026-01-13T14:04:22.200Z" π‘ Updating account 0197a1ea-0afb-711a-9b74-03460a24dff7 with score 70... β Successfully updated account 0197a1ea-0afb-711a-9b74-03460a24dff7 with CustomScore: 70
### Error Logs
β Validation failed: Missing currentImage object β CRM API call failed: CRM API Error (401): Unauthorized β Async webhook processing failed: Account ID: 0197a1ea-0afb-711a-9b74-03460a24dff7 ABC Classification: B Error: CRM API Error (401):
**Note:** Verbose payload logging has been removed for cleaner production logs
### Cloud Foundry (SAP BTP)
```bash
# Deploy the service
cf push
# Set environment variables
cf set-env crm-webhook-service CRM_BASE_URL "https://your-tenant.crm.cloud.sap"
cf set-env crm-webhook-service CRM_USERNAME "your-username"
cf set-env crm-webhook-service CRM_PASSWORD "your-password"
cf restage crm-webhook-service
# Check health
cf app crm-webhook-service
The service includes a health check endpoint for monitoring:
curl http://localhost:3000/healthResponse:
{
"status": "ok",
"service": "CRM Webhook Service",
"timestamp": "2026-01-13T10:37:27.666Z"
}crm-webhook-service/
βββ server.js # Main webhook service (~300 lines)
βββ package.json # Dependencies (express, dotenv)
βββ .env # Your credentials (git-ignored)
βββ env-template.txt # Template for .env
βββ manifest.yml # Cloud Foundry deployment config
βββ .gitignore # Excludes credentials and node_modules
βββ README.md # This documentation
"Missing required environment variables"
- Create
.envfile fromenv-template.txt - Ensure all variables are set (CRM_BASE_URL, CRM_USERNAME, CRM_PASSWORD)
Webhook not receiving requests
- Verify SAP CRM webhook configuration points to correct URL
- Check firewall/network allows SAP to reach your service
- Test locally with curl first
"CRM API Error (401)" - Unauthorized
- Verify credentials in
.envare correct - Check username/password have API access permissions
- Important: User must have write/update permissions for accounts in SAP CRM
- The user can read accounts (GET) but may lack permission to update (PATCH)
- Contact your SAP CRM administrator to grant proper authorization roles
"CRM API Error (404)" - Not Found
- Check webhook URL path is correct:
/webhooks/calculate-score-async(notasynch) - Verify the service is deployed and running
- Test the health endpoint first:
/health
"Missing currentImage object"
- Ensure webhook payload follows CloudEvents format with
datawrapper - Check that
data.currentImageexists in the payload - The service logs the full payload for debugging
"No ETag received from CRM"
- Ensure account exists in CRM
- Check CRM API is returning proper headers
- Verify account ID in webhook payload is correct
Async webhook fails silently
- Check server logs for detailed error messages:
cf logs crm-webhook-service --recent - Errors are logged with account ID and ABC classification
- Common causes: invalid ETag, account locked, network issues, 401 permission error
ABC classification not recognized
- Service defaults to "C" (score 50) for invalid values
- Check for typos in classification field (should be A, B, or C)
- Look for warning in logs:
β οΈ Unknown ABC classification
CORS errors in browser
- The service includes CORS headers for cross-origin requests
- If still encountering issues, check browser console for specific error
- Verify
Access-Control-Allow-Originis set to*in response headers
- β Synchronous webhook working correctly
- β CloudEvents format support working
- β CORS enabled and working
- β All features working correctly
- β Synchronous webhook working with full payload return
- β Asynchronous webhook with 10-second delay working
- β CloudEvents format support working
- β CORS enabled and working
- β ETag fetching and optimistic locking working
- β CRM updates via PATCH working (auth header bug fixed)
- β Clean operational logging without verbose payloads
- Fixed 401 Authorization Error - Headers now properly merged when adding
If-Matchfor PATCH requests - Simplified Logic - Removed prospect penalty, pure ABC classification scoring
- Clean Logging - Removed verbose payload dumps, keeping only operational logs
β Use synchronous when:
- Result needed before save completes
- Calculation is fast (<1 second)
- Want to block invalid data from being saved
- Need to return validation errors to user
β Don't use synchronous for:
- Long-running operations (>2 seconds)
- External API calls that might timeout
- Operations that can fail independently
β Use asynchronous when:
- Processing takes time (>1 second)
- Result not needed immediately
- Want to respond quickly and process later
- Can tolerate eventual consistency
β Don't use asynchronous for:
- Real-time validation
- Results needed in current transaction
- Critical business logic that must complete
# Install dependencies
npm install
# Start development server (local)
npm run dev
# Deploy to Cloud Foundry
cf push
# View live logs (streaming)
cf logs crm-webhook-service
# View recent logs (last events)
cf logs crm-webhook-service --recent
# Check app status and health
cf app crm-webhook-service
# Test health endpoint
curl http://localhost:3000/health
# Test sync webhook locally
curl -X POST http://localhost:3000/webhooks/calculate-score-sync \
-H "Content-Type: application/json" \
-d '{"data":{"currentImage":{"id":"test-123","customerABCClassification":"A"}}}'
# Set environment variables in Cloud Foundry
cf set-env crm-webhook-service CRM_USERNAME "new-username"
cf restage crm-webhook-service| Variable | Description | Example | Required |
|---|---|---|---|
CRM_BASE_URL |
SAP CRM tenant URL | https://my1000210.de1.demo.crm.cloud.sap |
Yes |
CRM_USERNAME |
API user with read/write access | api-user or dev |
Yes |
CRM_PASSWORD |
API user password | *** |
Yes |
PORT |
Server port (auto-set by Cloud Foundry) | 3000 or 8080 |
No |
NODE_ENV |
Environment (set in manifest.yml) | production |
No |
applications:
- name: crm-webhook-service
memory: 128M
instances: 1
buildpack: nodejs_buildpack
command: node server.js
env:
NODE_ENV: production
health-check-type: http
health-check-http-endpoint: /health- Runtime: Node.js 22.x (auto-detected by buildpack)
- Framework: Express 4.18.2
- Configuration: dotenv 16.3.1
- HTTP Client: Native Fetch API (Node.js built-in)
- Deployment: Cloud Foundry (SAP BTP)
- Microservice Architecture: Single-purpose service with clear API
- Webhook Pattern: Event-driven integration with SAP CRM
- Async Processing: Fire-and-forget background jobs with
setImmediate - Optimistic Locking: ETag-based concurrency control
- Basic Authentication: Simple, secure API access
- CORS Enabled: Cross-origin resource sharing for browser access
- Memory Usage: 23-25MB runtime, 128MB allocated
- Response Time: <50ms for webhook acceptance
- Processing Time: 10 seconds simulated delay + API calls (~10.5s total)
- Concurrency: Single instance, background tasks per request
- Response Time: <50ms for webhook acceptance
- Processing Time: 10 seconds simulated delay + API calls (~10.5s total)
- Concurrency: Single instance, background tasks per request
- Code Size: ~250 lines of clean, maintainable code
- Never logged or exposed in responses
- Credentials required for all CRM API calls
- β Environment variables for sensitive data
- β No credentials in code or version control
- β HTTPS enforced by Cloud Foundry router
- β Input validation on all webhook payloads
β οΈ Consider API key authentication for webhook endpoints in productionβ οΈ Consider IP whitelisting for webhook sources
- Add webhook signature verification
- Implement rate limiting
- Add authentication for webhook endpoints
- Use dedicated service account with minimal permissions
- Enable audit logging for compliance
- Implement retry logic with exponential backoff
- CRM Scoring Widget - Frontend widget for manual score editing with micro-frontend pattern
MIT