Skip to main content

V4 Participant API

The V4 Participant API provides improved performance and security for participant-facing endpoints through session-based authentication.

Overview

Why V4?

The V4 API improves on V3 with:

AspectV3V4
AuthenticationJWT token verification per requestSession-based (single verification)
PerformanceJWT decode overhead on each requestPre-authenticated session
Session trackingVia participant recordsDedicated session collection
Rate limitingPer-userPer-session with better controls
Feature flagAlways activeControlled rollout via USE_V4_PARTICIPANT_API

Feature Flag

V4 is controlled by a feature flag for gradual rollout:

// Backend environment variable
USE_V4_PARTICIPANT_API=true

// To disable V4 (revert to V3)
USE_V4_PARTICIPANT_API=false

Session-Based Authentication

How It Works

1. Participant joins experiment

2. Frontend calls POST /api/v4/participant/join

3. Backend creates session record in participantSessions collection

4. Backend returns sessionId and sessionToken

5. All subsequent requests include sessionId in header

6. Backend validates session (fast lookup, no JWT decode)

Session Lifecycle

// Session record in Firestore
// Collection: participantSessions/{sessionId}
{
sessionId: "sess_abc123",
participantId: "part_xyz789",
experimentId: "exp_research_001",
roomId: "room_live_456",
userId: "user_auth_123", // Firebase Auth UID
createdAt: Timestamp,
lastActivityAt: Timestamp,
expiresAt: Timestamp, // 24 hours from creation
ipAddress: "192.168.1.1",
userAgent: "Mozilla/5.0...",
status: "active" // active | completed | expired | revoked
}

Session TTL

  • Default TTL: 24 hours from creation
  • Activity extension: Last activity timestamp updated on each request
  • Cleanup: Expired sessions cleaned up via scheduled job

Endpoints

Discovery

Check available experiments and validate session:

GET /api/v4/participant/discover

Headers:

X-Session-Id: sess_abc123

Response:

{
"status": "success",
"data": {
"experiments": [
{
"experimentId": "exp_research_001",
"name": "Research Study 1",
"status": "recruiting",
"availableSlots": 5
}
],
"session": {
"valid": true,
"expiresIn": 82800
}
}
}

Join Experiment

Create a session and join an experiment:

POST /api/v4/participant/join
Content-Type: application/json

{
"experimentId": "exp_research_001",
"role": "participant",
"metadata": {
"source": "prolific",
"prolificPid": "abc123"
}
}

Headers:

Authorization: Bearer <firebase_id_token>

Response:

{
"status": "success",
"data": {
"sessionId": "sess_abc123",
"sessionToken": "stkn_xyz789...",
"participantId": "part_def456",
"roomId": "room_live_789",
"experimentConfig": {
"name": "Research Study 1",
"states": [...],
"globalComponents": [...]
},
"expiresAt": "2024-10-21T14:30:00.000Z"
}
}

Submit Events

Record experiment events:

POST /api/v4/participant/events
Content-Type: application/json

{
"events": [
{
"type": "state_transition",
"stateId": "state_intro",
"timestamp": 1697815800000,
"data": {
"fromState": null,
"toState": "introduction"
}
},
{
"type": "component_response",
"componentId": "rating_1",
"timestamp": 1697815850000,
"data": {
"value": 7,
"responseTime": 2500
}
}
]
}

Headers:

X-Session-Id: sess_abc123

Response:

{
"status": "success",
"data": {
"recorded": 2,
"serverTimestamp": "2024-10-20T14:30:50.123Z"
}
}

Complete Experiment

Mark participation as complete:

POST /api/v4/participant/complete
Content-Type: application/json

{
"completionCode": "STUDY123",
"finalState": "debriefing",
"summary": {
"totalEvents": 150,
"duration": 1800000
}
}

Headers:

X-Session-Id: sess_abc123

Response:

{
"status": "success",
"data": {
"completionCode": "STUDY123",
"redirectUrl": "https://prolific.com/return?code=STUDY123",
"sessionEnded": true
}
}

Session History

Get participant's session history (for returning participants):

GET /api/v4/participant/history

Headers:

Authorization: Bearer <firebase_id_token>

Response:

{
"status": "success",
"data": {
"sessions": [
{
"sessionId": "sess_abc123",
"experimentId": "exp_research_001",
"experimentName": "Research Study 1",
"status": "completed",
"startedAt": "2024-10-20T14:00:00.000Z",
"completedAt": "2024-10-20T14:30:00.000Z",
"completionCode": "STUDY123"
}
]
}
}

Rate Limiting

V4 implements per-session rate limiting:

EndpointLimit
/join5 per minute
/events100 per minute
/complete3 per minute
/discover20 per minute
/history10 per minute

Rate limit headers are included in responses:

X-RateLimit-Limit: 100
X-RateLimit-Remaining: 95
X-RateLimit-Reset: 1697815860

Migration from V3

Key Differences

  1. Authentication: Replace JWT verification with session ID
  2. Session creation: Call /join to get a session before other calls
  3. Event batching: V4 encourages batching events (array in single request)
  4. Completion flow: Explicit /complete endpoint instead of implicit

Migration Steps

  1. Enable feature flag in staging:

    USE_V4_PARTICIPANT_API=true
  2. Update frontend to use session-based flow:

    // Old (V3)
    const response = await fetch('/api/v3/participant/events', {
    headers: { 'Authorization': `Bearer ${idToken}` }
    });

    // New (V4)
    const session = await joinExperiment(experimentId, idToken);
    const response = await fetch('/api/v4/participant/events', {
    headers: { 'X-Session-Id': session.sessionId }
    });
  3. Test thoroughly with real participants

  4. Monitor performance in production

  5. Full rollout when stable

Fallback Behavior

When V4 is disabled, requests automatically fall back to V3:

// Backend routing
if (process.env.USE_V4_PARTICIPANT_API === 'true') {
app.use('/api/v4/participant', v4ParticipantRoutes);
} else {
// V4 routes redirect to V3 equivalents
app.use('/api/v4/participant', (req, res, next) => {
req.url = req.url.replace('/v4/', '/v3/');
next();
});
}

Error Handling

V4-Specific Errors

Error CodeDescriptionSolution
SESSION_EXPIREDSession has expiredRe-authenticate with /join
SESSION_INVALIDSession not found or revokedRe-authenticate with /join
SESSION_MISMATCHSession doesn't match experimentUse correct session
EXPERIMENT_CLOSEDExperiment no longer accepting participantsN/A

Error Response Format

{
"status": "error",
"error": {
"code": "SESSION_EXPIRED",
"message": "Session has expired. Please rejoin the experiment.",
"details": {
"sessionId": "sess_abc123",
"expiredAt": "2024-10-20T14:30:00.000Z"
}
}
}

Implementation Files

FilePurpose
backend/src/api/v4/routes/participant.jsV4 endpoint handlers
backend/src/api/v4/middleware/sessionAuth.jsSession validation middleware
backend/src/services/participantSessionService.jsSession CRUD operations
frontend/src/lib/services/participantService.jsFrontend V4 client

Security Considerations

Session Security

  • Session IDs: Cryptographically random, 32 bytes
  • Session tokens: Signed with server secret, validated on each request
  • IP binding: Optional - session can be bound to originating IP
  • User agent validation: Detect session hijacking attempts
  1. Store session ID in memory only (not localStorage)
  2. Re-authenticate on page refresh
  3. Implement session timeout warnings for long experiments
  4. Use HTTPS exclusively