Skip to main content
You don’t need to store every field from the webhook event. This page shows a recommended flat record, a complete save-and-acknowledge pattern, and the database indexes to add.
ColumnTypeSource fieldNotes
event_idvarchar PKeventIdUnique key — enables idempotent upsert
candidate_emailvarcharcandidateEmailUse to join back to your own users
candidate_namevarcharcandidateName
assessment_idvarcharassessmentId
assessment_namevarcharassessmentName
job_titlevarcharjobTitle
scoreintscore
total_marksinttotalMarks
pass_marksintpassMarks
passedboolpassed
statusvarcharstatuscompleted / timeout / abandoned
submitted_attimestampsubmittedAt
duration_minutesint nullabledurationMinutes
ai_feedbacktext nullableaiFeedback
report_urlvarcharreportUrl
proctoring_scoreintproctoring.scoreQuick filter without JSON parsing
proctoring_violation_countintproctoring.violationCount
stored_attimestampstoredAt
Store the full questionAnswers JSON if you need per-question drill-down in your UI. Store proctoring.recentViolations if you need to show recruiters a violation timeline.

Minimum required fields

At a minimum you need these five fields to be useful:
event_id       → idempotency
candidate_email → link to your user
score / totalMarks / passed → the result
report_url      → link recruiter to full report

Complete save + acknowledge pattern

async function pollAndSave() {
  const { events } = await client.getWebhookEvents({ limit: 50 });
  if (!events.length) return;

  const processedIds: string[] = [];

  for (const event of events) {
    try {
      await prisma.assessmentResult.upsert({
        where:  { eventId: event.eventId },
        create: {
          eventId:                  event.eventId,
          candidateEmail:           event.candidateEmail,
          candidateName:            event.candidateName ?? '',
          assessmentId:             event.assessmentId,
          assessmentName:           event.assessmentName ?? '',
          jobTitle:                 event.jobTitle ?? '',
          score:                    event.score,
          totalMarks:               event.totalMarks,
          passMarks:                event.passMarks,
          passed:                   event.passed,
          status:                   event.status,
          submittedAt:              new Date(event.submittedAt),
          durationMinutes:          event.durationMinutes ?? null,
          aiFeedback:               event.aiFeedback ?? null,
          reportUrl:                event.reportUrl,
          proctoringScore:          event.proctoring?.score ?? 0,
          proctoringViolationCount: event.proctoring?.violationCount ?? 0,
          storedAt:                 new Date(event.storedAt),
        },
        update: {}, // don't overwrite — idempotent
      });

      processedIds.push(event.eventId);
    } catch (err) {
      console.error('Failed to save event', event.eventId, err);
      // Don't push to processedIds — will retry next poll
    }
  }

  if (processedIds.length) {
    await client.acknowledgeWebhookEvents(processedIds);
  }
}

Database indexes

-- Unique constraint for idempotency (required)
CREATE UNIQUE INDEX idx_assessment_results_event_id
  ON assessment_results (event_id);

-- Fast lookup by candidate email (required)
CREATE INDEX idx_assessment_results_candidate_email
  ON assessment_results (candidate_email);

-- Filter passed / failed quickly
CREATE INDEX idx_assessment_results_passed
  ON assessment_results (passed);

-- Sort by submission date
CREATE INDEX idx_assessment_results_submitted_at
  ON assessment_results (submitted_at DESC);

Prisma schema reference

model AssessmentResult {
  id                       String    @id @default(cuid())
  eventId                  String    @unique
  candidateEmail           String
  candidateName            String
  assessmentId             String
  assessmentName           String
  jobTitle                 String
  score                    Int
  totalMarks               Int
  passMarks                Int
  passed                   Boolean
  status                   String
  submittedAt              DateTime
  durationMinutes          Int?
  aiFeedback               String?
  reportUrl                String
  proctoringScore          Int
  proctoringViolationCount Int
  storedAt                 DateTime
  createdAt                DateTime  @default(now())

  @@index([candidateEmail])
  @@index([passed])
  @@index([submittedAt(sort: Desc)])
}