You add proctoring to an Angular app with the same framework-agnostic SDK everyone else uses: mint a session on your backend, then call ProctorLink.createSession({ jwt, sessionId }).start() from a component’s ngOnInit (usually through an injectable service) and call stop() then destroy() in ngOnDestroy. There is no Angular-specific package. The work is holding the session in a service, wiring events once, and letting ngOnDestroy end the attempt. Every call below comes from the ProctorLink API reference.
The SDK is imperative and the camera lives in a cross-origin iframe enclave that mounts itself, so it does not belong in a template. It belongs in a component’s lifecycle hooks, and the session itself belongs in an injectable service. Angular’s job is to tell you when the exam component appears and disappears; the SDK’s job is to start when it appears and tear down when it goes. Map each SDK call to the Angular moment that should trigger it and the integration writes itself.
| Angular moment | What you do | Call |
|---|---|---|
| Before the exam route loads | Mint a session on your backend | POST /v1/sessions |
| Component ngOnInit | Create the session and begin capture | ProctorLink.createSession({ jwt, sessionId }).start() |
| Component ngOnDestroy | Flush and tear down | stop() then destroy() |
| On the token-expired event | Re-mint for the same attempt_id, swap the token | updateToken(jwt) |
| After the candidate submits | Read the integrity report on your backend | GET /v1/sessions/:id |
This is the same three-part split covered in How to add proctoring to a web application, applied to Angular’s lifecycle. If your exams run in a different framework, the React assessment app guide covers the same calls with hooks. If you are still choosing between the SDK, the REST API, and the Moodle plugin, start with Proctoring SDK vs API vs LMS plugin. New to the field? Read What is online proctoring? and What is AI proctoring? first.
An Angular component cannot hold your API key safely, so minting stays on the server. Your Angular app calls its own backend endpoint through HttpClient, and that endpoint calls POST /v1/sessions. Send a stable attempt_id derived from your own attempt record, because that is what lets a refresh resume the same session instead of starting a new one.
// Your backend, or a server route the Angular app calls.
// The access-token and secret-token never reach the browser.
POST /v1/sessions
access-token: <YOUR_ACCESS_TOKEN>
secret-token: <YOUR_SECRET_TOKEN>
content-type: application/json
{
"external_user_id": "candidate-123",
"exam_id": "math-101-final",
"attempt_id": "attempt-789",
"allowed_origins": ["https://exams.yourcompany.com"]
}
// Response - pass only these to the Angular client
{
"session_id": "6a7b18df70e4f8ecf2597b6f",
"session_jwt": "eyJhbGciOi...",
"expires_at": 1786443201
}The browser only ever receives the session_jwt and session_id. Your access-token and secret-token stay on the server. The token lasts two hours by default; set ttl_seconds to your exam length plus a buffer and read the returned expires_at, because values outside the allowed range are clamped rather than rejected.
Put the whole lifecycle in one service so your exam component stays about questions, not cameras. Create the session inside a start() method, hold the instance on the service, wire the events once, and expose a stop() the component can call. A service also survives navigation when you provide it at the right level, which matters when one attempt spans several routes.
npm install @proctorlink/sdkThe loader is published on npm as @proctorlink/sdk, so it installs like any other dependency and works with npm, pnpm, or yarn. You get your API keys when you sign up at app.proctorlink.com.
// proctoring.service.ts
import { Injectable, NgZone } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { firstValueFrom } from 'rxjs';
import { ProctorLink } from '@proctorlink/sdk';
type MintResult = { session_jwt: string; session_id: string; resumed?: boolean };
@Injectable({ providedIn: 'root' })
export class ProctoringService {
private session: ReturnType<typeof ProctorLink.createSession> | null = null;
constructor(private http: HttpClient, private zone: NgZone) {}
async start(jwt: string, sessionId: string, attemptId: string) {
const session = ProctorLink.createSession({ jwt, sessionId });
this.session = session;
session.on('permission', ({ camera }) => {
if (camera === 'denied') {
// Update the view from inside the zone so change detection runs.
this.zone.run(() => { /* block or warn, your policy */ });
}
});
session.onEvent((event) => console.log(event.type, event));
// Safety net for exams that outlast the token.
session.on('token-expired', async () => {
const res = await this.remint(attemptId);
if (res.resumed) {
session.updateToken(res.session_jwt); // same session, carry on
}
// If res.resumed is false the old session ended; start a fresh one.
});
await session.start();
}
stop() {
this.session?.stop();
this.session?.destroy();
this.session = null;
}
private remint(attemptId: string): Promise<MintResult> {
// Your own backend endpoint that calls POST /v1/sessions.
return firstValueFrom(
this.http.post<MintResult>('/api/proctoring/session', { attempt_id: attemptId }),
);
}
}Two events earn a listener. The permission event reports whether the candidate granted the camera, so you can enforce your own policy. The token-expired event fires only if a token dies mid-attempt, so capture never fails silently. Both callbacks can run outside Angular’s zone, so wrap any part that updates the view in NgZone.run(). The default flow records as soon as the camera is granted and uses the first captured frame as the identity reference. If you want matching against a deliberate photo instead, create the session with autoStartCapture: false, call captureIdentity() after start() resolves, then call beginCapture().
The exam component just calls the service. Start proctoring in ngOnInit, and stop it in ngOnDestroy. When the candidate submits, navigates away, or the router swaps the route, Angular destroys the component and runs ngOnDestroy, which calls stop() then destroy() through the service. That is the whole reason to lean on ngOnDestroy: it fires on every way out, so you never leave an enclave running.
// proctored-exam.component.ts
import { Component, Input, OnDestroy, OnInit } from '@angular/core';
import { ProctoringService } from './proctoring.service';
@Component({
selector: 'app-proctored-exam',
standalone: true,
templateUrl: './proctored-exam.component.html',
})
export class ProctoredExamComponent implements OnInit, OnDestroy {
@Input() session!: { jwt: string; id: string }; // fetched from your backend
@Input() attempt!: { id: string }; // your own attempt record
constructor(private proctoring: ProctoringService) {}
ngOnInit(): void {
// Camera comes up as its own floating enclave; render your exam freely.
this.proctoring.start(this.session.jwt, this.session.id, this.attempt.id);
}
ngOnDestroy(): void {
this.proctoring.stop(); // stop() then destroy(), inside the service
}
}If your page sets a Content-Security-Policy, allow the enclave origin so the iframe can mount:
frame-src https://enclave.proctorlink.com;The session token is short-lived. For an exam that can outlast it, the token-expired handler in the service re-mints on your backend for the same attempt_id. Because that attempt still has an active session, ProctorLink returns the existing session_id with a fresh token and resumed set to true, and you pass the new token to updateToken(). Events queued while the token was dead are flushed as soon as it lands, so nothing from the gap is lost.
Check resumed before calling updateToken(). A re-mint only resumes while the session is still active; if the abandonment sweep has already closed it, the mint creates a new session whose token belongs to a different attempt. The simplest way to avoid the whole situation is to set ttl_seconds generously at mint and treat token-expired as a safety net.
After the attempt, your backend calls GET /v1/sessions/:id. Face analysis is deferred, not real-time, so fetch the report once status is validated, or equivalently when integrity.analysis_complete is true. Before that, face counts are zero and identity_match is pending, which is an unfinished result rather than a clean one.
// Your backend. Fetch once status is "validated"
// (or integrity.analysis_complete is true).
GET /v1/sessions/6a7b18df70e4f8ecf2597b6f
access-token: <YOUR_ACCESS_TOKEN>
secret-token: <YOUR_SECRET_TOKEN>
// Response (trimmed)
{
"status": "validated",
"integrity": {
"score": 100,
"level": "low",
"flagged": false,
"identity_match": "pass",
"reasons": []
}
}The integrity.score starts at 100 and drops as penalties apply. The level is low at 80 or above, medium from 50 to 79, and high below 50, and flagged turns true below 80. The verdict is computed server-side only, so a candidate cannot influence it from the Angular app no matter what they do in the browser. A score alone should not fail anyone: use reasons and evidence to drive human review of anything flagged.
ngOnInit (or behind isPlatformBrowser), never in the constructor or a class field.providers is destroyed and recreated on navigation, so a multi-route attempt tears the session down and re-mints. Fix: provide it with providedIn: 'root' or at a parent that survives the whole attempt.ngOnDestroy cleanup. Without a hook that calls stop() then destroy(), navigating between exams leaves an enclave running and can stack a second camera preview. Fix: implement OnDestroy and stop the session there.NgZone. The permission, event, and token-expired callbacks can fire outside Angular’s zone, so a field you set from them may not refresh the template. Fix: wrap the UI update in NgZone.run(), or call markForCheck() with OnPush.attempt_id on init. A value created with crypto.randomUUID() in ngOnInit defeats resume, so every refresh mints a new session and bills the attempt twice. Fix: derive attempt_id from your own attempt record so it is stable for the attempt.ngOnDestroy, which ends the attempt server-side, and the next mint creates a separate session: two reports for one attempt. Fix: handle token-expired and call updateToken() with a freshly minted token for the same attempt_id.The split matters more in a single-page app, where it is tempting to keep everything in the client. Your API key is server-side only. The browser receives a short-lived, origin-bound session token, and that token lives inside the cross-origin iframe enclave served from ProctorLink’s own domain rather than in an Angular service field or component input. The camera permission binds to the enclave origin, so a candidate who has already granted it on one exam is not prompted again on another site that embeds ProctorLink. The integrity score is computed server-side only, which is the core anti-tamper property: a candidate cannot edit their own score from the browser console.
Keyframes upload from the browser straight to object storage through presigned URLs, so image bytes never transit the API, and only periodic stills are captured rather than continuous video. If your exams run inside Moodle instead of your own Angular app, you can skip this integration and use the drop-in plugin covered in Best Moodle proctoring plugin. Across published deployments, ProctorLink has supported more than one million proctored exam sessions (methodology note below). Full cohort sizes and outcomes are on the case studies page.
Institutions evaluating proctoring tools often look for independent feedback outside vendor case studies. ProctorLink is listed on G2, where Moodle administrators and training teams share verified product reviews.
Read ProctorLink reviews on G2 →Deployment statistics and product behaviour described in this guide link to the sources below.
Ready to proctor exams in your Angular app? Sign up at app.proctorlink.com to get your API keys, wire the service above into a real attempt, and check the reviewer workload before you commit.
Create an account at app.proctorlink.com to get your SDK credentials and start minting sessions.
Install @proctorlink/sdk, the browser loader that mounts the enclave and captures keyframes.
See the browser SDK and integrity report in a 30-minute developer walkthrough.
Per-session and subscription models for custom applications and LMS exams.
The framework-agnostic version of this guide, with the same three calls.
The same integration for a React assessment app, using a custom hook.
Back to the knowledge hub for the full SDK and proctoring series.
Online proctoring supervises remote exams via webcam and microphone inside your LMS, logging rule violations with timestamps so reviewers judge flagged cases, not every session.
AI proctoring uses machine learning to automatically detect suspicious exam behaviour such as multiple faces, tab switching, and absence from the camera.
A comparison of leading Moodle proctoring plugins for universities that need native LMS integration, AI monitoring, and institution-owned data.
How university exam offices choose online proctoring software for entrance grids, finals, and multi-faculty calendars, including stakeholder RFPs and centre-vs-online cost tradeoffs.
How certification bodies and training providers use online proctoring for identity proofing, item-bank protection, and defensible, audit-ready credentialing exams.
A side-by-side comparison of AI proctoring and live human proctoring for cost, scale, accuracy, and exam security.
Practical steps to reduce cheating in Moodle quizzes using proctoring settings, question banks, timing controls, and AI monitoring.
Feature-by-feature comparison of Moodle proctoring plugins covering integration, AI detection, pricing, and data storage.
A guide to online exam proctoring for Indian universities and certification providers, including compliance, pricing in INR, and local case studies.
How to choose between a proctoring LMS plugin, a browser SDK, and a REST API when adding proctoring to software you already own, with a decision guide and code examples.
A step-by-step guide to adding exam proctoring to a web app you own: mint a session on your backend, run the browser SDK, and read a server-side integrity report, with code from the API reference.
What browser-based proctoring can detect (tab switches, clipboard, faces, identity) and what it cannot (phones, a second monitor, a virtual machine), and why the tiering exists.
A step-by-step guide to adding exam proctoring to a React app: mint a session on your backend, wrap the browser SDK in a useEffect hook, handle StrictMode and token expiry, and read a server-side integrity report.
Your backend mints a session, an injectable service runs it across the component lifecycle, and you read a server-side integrity score. Pilot the SDK on a real attempt and check the reviewer workload before you commit.