---
title: Best Practices
url: https://docs.kaltura.com/docs/events-collaboration/user-profile-api/user-profile-best-practices
---

## Best Practices

- **Use `bulkAdd` for batch registration.** Up to 50 profiles per request — significantly faster than individual calls for event registration imports.
- **Resolve virtual event ID → appGuid via App Registry first.** Use `appCustomIdIn` filter to map event IDs to app GUIDs before managing profiles (see [App Registry API](/docs/events-collaboration/app-registry-api/app-registry-overview)).
- **Use status transitions to track attendance lifecycle.** Progress users through `created → registered → confirmed → attended → participated` for accurate reporting.
- **Use `getFiltered` for reporting and analytics.** Filter by status, date range, and fields to build attendance dashboards without downloading all profiles.
- **Use AppTokens for production access.** Generate KS via `appToken.startSession` with HMAC — keep admin secrets off application servers.

### Common Integration Patterns

#### Event Registration Flow

```bash
# 1. Register the app in App Registry (one-time setup)
APP_GUID=$(curl -s -X POST "$KALTURA_APP_REGISTRY_URL/app-registry/add" \
  -H "Authorization: Bearer $KALTURA_KS" \
  -H "Content-Type: application/json" \
  -d '{"appCustomId": "webinar-2025-q4", "appType": "ep", "appCustomName": "Q4 Webinar"}' \
  | jq -r '.id')

# 2. Create user profile when they register
PROFILE_ID=$(curl -s -X POST "$KALTURA_USER_PROFILE_URL/user-profile/add" \
  -H "Authorization: Bearer $KALTURA_KS" \
  -H "Content-Type: application/json" \
  -d "{
    \"appGuid\": \"$APP_GUID\",
    \"userId\": \"attendee@example.com\",
    \"profileData\": {\"name\": \"John Smith\", \"company\": \"Tech Corp\"},
    \"eventData\": {
      \"regOrigin\": \"registration\",
      \"attendanceStatus\": \"registered\",
      \"userRegistrationType\": \"virtualAttendanceRequest\"
    }
  }" | jq -r '.id')

# 3. Confirm attendance
curl -s -X POST "$KALTURA_USER_PROFILE_URL/user-profile/update" \
  -H "Authorization: Bearer $KALTURA_KS" \
  -H "Content-Type: application/json" \
  -d "{
    \"id\": \"$PROFILE_ID\",
    \"eventData\": {\"attendanceStatus\": \"confirmed\"}
  }"

# 4. Mark as attended (during event — triggers firstAttendedStatusTime)
curl -s -X POST "$KALTURA_USER_PROFILE_URL/user-profile/update" \
  -H "Authorization: Bearer $KALTURA_KS" \
  -H "Content-Type: application/json" \
  -d "{
    \"id\": \"$PROFILE_ID\",
    \"eventData\": {\"attendanceStatus\": \"attended\"}
  }"

# 5. Get attendance stats
curl -s -X POST "$KALTURA_USER_PROFILE_URL/reports/eventDataStats" \
  -H "Authorization: Bearer $KALTURA_KS" \
  -H "Content-Type: application/json" \
  -d "{
    \"filter\": {\"appGuidIn\": [\"$APP_GUID\"]},
    \"dimensions\": [\"attendanceStatus\"]
  }"
```

#### Bulk Import Attendees

```bash
# Import a batch of invited users
curl -X POST "$KALTURA_USER_PROFILE_URL/user-profile/bulkAdd" \
  -H "Authorization: Bearer $KALTURA_KS" \
  -H "Content-Type: application/json" \
  -d "[
    {\"appGuid\": \"$APP_GUID\", \"userId\": \"vip1@example.com\", \"profileData\": {\"name\": \"VIP One\"}, \"eventData\": {\"regOrigin\": \"invite\", \"attendanceStatus\": \"invited\"}},
    {\"appGuid\": \"$APP_GUID\", \"userId\": \"vip2@example.com\", \"profileData\": {\"name\": \"VIP Two\"}, \"eventData\": {\"regOrigin\": \"invite\", \"attendanceStatus\": \"invited\"}}
  ]"
```

#### Cross-Service Registration Data Retrieval

The full workflow for extracting registration data for a virtual event spans multiple services. The key linkage is that `appCustomId` in the App Registry equals the `virtualEventId`:

```bash
# Step 1: List virtual events to get event IDs
VIRTUAL_EVENT_ID=$(curl -s -X POST "$KALTURA_SERVICE_URL/service/virtualevent_virtualevent/action/list" \
  -d "ks=$KALTURA_KS" -d "format=1" | jq -r '.objects[0].id')

# Step 2: Resolve virtual event ID → App GUID via App Registry
APP_GUID=$(curl -s -X POST "$KALTURA_APP_REGISTRY_URL/app-registry/list" \
  -H "Authorization: Bearer $KALTURA_KS" \
  -H "Content-Type: application/json" \
  -d "{\"filter\": {\"appCustomIdIn\": [\"$VIRTUAL_EVENT_ID\"]}}" \
  | jq -r '.objects[0].id')

# Step 3: Pull registration/attendance data
RESPONSE=$(curl -s -X POST "$KALTURA_USER_PROFILE_URL/user-profile/list" \
  -H "Authorization: Bearer $KALTURA_KS" \
  -H "Content-Type: application/json" \
  -d "{
    \"filter\": {\"appGuidIn\": [\"$APP_GUID\"]},
    \"pager\": {\"offset\": 0, \"limit\": 500},
    \"orderBy\": \"createdAt\",
    \"includeTotalCount\": true
  }")

# Step 4: Join with core user data for complete profiles
# User Profile has eventData (attendance) and profileData (custom fields)
# Core fields (email, firstName, lastName, company, jobTitle) are in user.list
USER_IDS=$(echo "$RESPONSE" | jq -r '[.objects[].userId] | join(",")')
curl -s -X POST "$KALTURA_SERVICE_URL/service/user/action/list" \
  -d "ks=$KALTURA_KS" -d "format=1" \
  -d "filter[objectType]=KalturaUserFilter" \
  -d "filter[idIn]=$USER_IDS"
```

See the [App Registry API](/docs/events-collaboration/app-registry-api/app-registry-overview) section 12.4 for the complete cross-service diagram.

#### Incremental Data Pull

Use `updatedAtGreaterThanOrEqual` to pull only profiles changed since your last sync — essential for large-scale integrations:

```bash
# Pull profiles updated since your last watermark
LAST_SYNC="2025-06-14T00:00:00Z"
curl -s -X POST "$KALTURA_USER_PROFILE_URL/user-profile/list" \
  -H "Authorization: Bearer $KALTURA_KS" \
  -H "Content-Type: application/json" \
  -d "{
    \"filter\": {
      \"appGuidIn\": [\"$APP_GUID\"],
      \"updatedAtGreaterThanOrEqual\": \"$LAST_SYNC\"
    },
    \"pager\": {\"offset\": 0, \"limit\": 500},
    \"orderBy\": \"updatedAt\"
  }"
# Store the last record's updatedAt as your new watermark
```

For very large datasets (10K+ records), combine pagination with time-based windowing — use `updatedAtLessThanOrEqual` alongside `updatedAtGreaterThanOrEqual` to partition queries into daily or hourly windows.

#### Paginated Export

```bash
OFFSET=0
LIMIT=100
while true; do
  RESPONSE=$(curl -s -X POST "$KALTURA_USER_PROFILE_URL/user-profile/list" \
    -H "Authorization: Bearer $KALTURA_KS" \
    -H "Content-Type: application/json" \
    -d "{
      \"filter\": {\"appGuidIn\": [\"$APP_GUID\"]},
      \"pager\": {\"offset\": $OFFSET, \"limit\": $LIMIT},
      \"orderBy\": \"createdAt\"
    }")

  COUNT=$(echo "$RESPONSE" | jq '.objects | length')
  echo "Fetched $COUNT profiles (offset=$OFFSET)"

  # Process profiles...

  [ "$COUNT" -lt "$LIMIT" ] && break
  OFFSET=$((OFFSET + LIMIT))
done
```

#### Re-register After Cancellation

After a user unregisters and their profile is soft-deleted, create a new profile to re-register them:

```bash
# Original profile was deleted (soft-delete)
# Create a fresh profile for the same user + app
curl -X POST "$KALTURA_USER_PROFILE_URL/user-profile/add" \
  -H "Authorization: Bearer $KALTURA_KS" \
  -H "Content-Type: application/json" \
  -d "{
    \"appGuid\": \"$APP_GUID\",
    \"userId\": \"returning-user@example.com\",
    \"profileData\": {\"name\": \"Returning User\"},
    \"eventData\": {
      \"regOrigin\": \"registration\",
      \"attendanceStatus\": \"registered\"
    }
  }"
```


#### PII Deletion and Cleanup

When deleting event data for compliance (GDPR, data retention), follow this order:

1. **Resolve the event** — Get the `appGuid` from App Registry using `appCustomIdIn: [virtualEventId]`
2. **List user profiles** for the event — `user-profile/list` with `appGuidIn: [appGuid]`
3. **Check multi-event registration** — For each userId, query `user-profile/list` *without* appGuid filter to find profiles in other events. Flag users registered to multiple events.
4. **Delete user profiles** — `user-profile/delete` for each profile in this event
5. **Delete KalturaUsers** — Only delete users from `user.delete` if they have **no profiles in other events**. Flagged users retain their core user record.

Delete user profiles before deleting the underlying KalturaUser — this preserves the ability to re-invite the user to future events. The user profile creation step validates that the user exists via `user.get`, so the core user record must still be present when creating new profiles.

```bash
# Delete a profile, then check if the user can be fully deleted
curl -s -X POST "$KALTURA_USER_PROFILE_URL/user-profile/delete" \
  -H "Authorization: Bearer $KALTURA_KS" \
  -H "Content-Type: application/json" \
  -d "{\"id\": \"$PROFILE_ID\"}"

# Check if user has profiles in other events before deleting the core user
OTHER_PROFILES=$(curl -s -X POST "$KALTURA_USER_PROFILE_URL/user-profile/list" \
  -H "Authorization: Bearer $KALTURA_KS" \
  -H "Content-Type: application/json" \
  -d "{
    \"filter\": {\"userIdIn\": [\"$USER_ID\"]},
    \"pager\": {\"offset\": 0, \"limit\": 1},
    \"includeTotalCount\": true
  }" | jq '.totalCount')

if [ "$OTHER_PROFILES" -eq 0 ]; then
  # Safe to delete the core user
  curl -s -X POST "$KALTURA_SERVICE_URL/service/user/action/delete" \
    -d "ks=$KALTURA_KS" -d "format=1" -d "userId=$USER_ID"
fi
```


### Registration Reports

For CSV-based registration and engagement reports, the platform provides an async Reports Service. This is separate from the User Profile API's `eventDataStats` endpoint and produces downloadable CSV files.

**Reports Base URL:** `https://reports.nvp1.ovp.kaltura.com/api/v1`  
**Auth:** `Authorization: Bearer &lt;KS&gt;`

#### Generate a Report

```bash
REPORTS_URL="https://reports.nvp1.ovp.kaltura.com/api/v1"

# Request report generation
SESSION_ID=$(curl -s -X POST "$REPORTS_URL/report/generate" \
  -H "Authorization: Bearer $KALTURA_KS" \
  -H "Content-Type: application/json" \
  -d "{
    \"reportParameters\": {\"appGuid\": \"$APP_GUID\"},
    \"reportName\": \"registration\"
  }" | jq -r '.sessionId')
```

#### Poll and Download

The generate call returns a `sessionId`. Poll the serve endpoint until the report is ready:

```bash
# Poll until ready (statusOnly: true returns status without CSV)
while true; do
  STATUS=$(curl -s -X POST "$REPORTS_URL/report/serve" \
    -H "Authorization: Bearer $KALTURA_KS" \
    -H "Content-Type: application/json" \
    -d "{\"sessionId\": \"$SESSION_ID\", \"statusOnly\": true}" \
    | jq -r '.status')

  [ "$STATUS" = "completed" ] && break
  sleep 2
done

# Download the CSV
curl -s -X POST "$REPORTS_URL/report/serve" \
  -H "Authorization: Bearer $KALTURA_KS" \
  -H "Content-Type: application/json" \
  -d "{\"sessionId\": \"$SESSION_ID\", \"statusOnly\": false}"
```

Reports may reflect cached data with a freshness window of 10-15 minutes. For real-time attendance status, use the User Profile `list` endpoint directly.

#### Engagement Reports (API v3)

Per-user session engagement data is available via the core Kaltura Reports API using specific report IDs:

| Report ID | Purpose | Key Fields |
|-----------|---------|------------|
| 3030 | Live/VOD engagement summary | user_id, email, plays, sum_minutes_viewed, live_engagement_rate |
| 3035 | Device/OS breakdown | device_used, os_used, view_time, total_completion_rate |
| 3037 | Per-user session detail | date, user_id, entry_id, entry_name, sum_minutes_viewed |
| 6000 | Meeting Room session reports | entry_id, entry_name, session_start_time, session_end_time |
| 6001 | Meeting Room attendee reports | entry_id, user_id, user_name, user_type, meeting_view_time |

```bash
# Get per-user session engagement for a virtual event
curl -s -X POST "$KALTURA_SERVICE_URL/service/report/action/getCsvFromStringParams" \
  -d "ks=$KALTURA_KS" \
  -d "format=1" \
  -d "id=3037" \
  -d "params=from_date=$FROM_UNIX;to_date=$TO_UNIX;virtual_event_ids=$VIRTUAL_EVENT_ID"
```
