Skip to main content

NESA Teacher Registration Verification

Teacher Registration Overview

For an overview of teacher registration verification across all states, see the Teacher Registration Guide. This guide provides detailed NSW-specific information.

This comprehensive guide shows you how to integrate NESA (NSW Education Standards Authority) teacher registration verification using the Oho API.

Overview

The /api/scan endpoint provides verification of NSW teacher registration through NESA. Validations are processed asynchronously - the endpoint queues the request and returns immediately with a correlation_id for tracking. Results are delivered later via webhook.

NESA (NSW Education Standards Authority) maintains the register of accredited teachers in New South Wales. Oho provides verification to confirm current teacher registration status.

Base URL: POST /api/scan

Authentication: Required (Bearer token)

Content-Type: application/json

Key Benefits:

  • ✅ Verification of NSW teacher registration
  • ✅ Instant status confirmation (typically less than 5 seconds)
  • ✅ Asynchronous processing for high performance
  • ✅ Built-in retry logic and webhook delivery

What is NESA?

NESA (NSW Education Standards Authority) is the regulatory body responsible for teacher registration and accreditation in New South Wales. All teachers in NSW government and non-government schools must be registered with NESA.

Registration Categories:

  • Graduate Teacher
  • Proficient Teacher
  • Highly Accomplished Teacher
  • Lead Teacher

Registration Types:

  • Full registration
  • Conditional registration
  • Provisional registration

Understanding NESA Verification

Important Limitation

NESA verification has a key limitation: The NESA registry only indicates if a registration is "Active" or not. Oho performs additional validation to determine the actual status.

What this means:

  • registry_response.response will always show ["Active"] if found in the NESA register
  • The actual validation result comes from status_color and status_flags
  • Oho validates by checking if the teacher details match NESA records

Status Interpretation

Status ColorMeaningstatus_flagsmeta.status
greenValid and currentEmpty {}found: true, current: true
yellowNeeds attentionexpired_unconfirmedfound: true, current: true
redInvalid/Not foundnot_currentfound: false, current: false

Critical: Always check status_color and status_flags - do NOT rely solely on registry_response.response.


Getting Started

Prerequisites

Before submitting NESA verifications, ensure:

  1. Active organization account - Your organization must be active and in good standing
  2. Verified user status - Your API user must have completed email verification
  3. Constituents created (optional) - If linking to existing constituents, they must exist in your organization first

Payload Structure

The NESA verification endpoint uses a simple payload structure:

{
"type": "nesa",
"identifier": "NESA_ACCREDITATION_NUMBER",
"first_name": "First",
"middle_name": "Middle (optional)",
"surname": "Last",
"birth_date": "YYYY-MM-DD (optional)",
"constituent": {
"id": 123
}
}

Field Descriptions

FieldTypeDescriptionRequired
typestringMust be "nesa"Yes
identifierstringNESA accreditation numberYes
first_namestringFirst/given nameYes
middle_namestringMiddle name(s)No
surnamestringSurname/family nameYes
birth_datestring (YYYY-MM-DD)Date of birthNo
constituent.idintegerLink to existing constituentNo

NESA Accreditation Number Format

NESA accreditation numbers are unique identifiers assigned to registered teachers in NSW.

Format: Typically numeric

Example: 123456

Finding Accreditation Numbers: Teachers can find their NESA accreditation number:

  1. On their NESA registration certificate
  2. In the NESA portal
  3. On correspondence from NESA

Tracking Requests

Correlation ID

When you submit a scan request, the API returns a correlation_id:

{
"correlation_id": "d591aa45-64fa-46b9-8eab-423075ee66cb"
}

Important: The correlation_id does not persist from the initial request to the webhook response. You cannot rely on it to match webhook results back to your original requests.

Matching Webhook Results

To match webhook results back to your original requests, use the NESA accreditation number (identifier):

Your tracking approach should be:

  1. Store the identifier (NESA number) with your internal records when submitting a scan
  2. When you receive a webhook, match it using content.current.identifier
  3. Use content.current.id (the accreditation ID) for long-term references

Example tracking pattern:

// When submitting scan
await database.savePendingScan({
nesa_number: '123456',
constituent_id: 12345,
status: 'pending',
submitted_at: new Date()
});

// When receiving webhook
async function handleWebhook(webhookData) {
const identifier = webhookData.content.current.identifier;

// Find your record by the NESA number
const scan = await database.findByIdentifier(identifier);

// Update with results
await database.updateScan(scan.id, {
accreditation_id: webhookData.content.current.id,
status: webhookData.content.current.status,
status_color: webhookData.content.current.status_color,
registry_response: webhookData.content.current.registry_response
});
}

Quick Start Example

Basic NESA Verification

curl -X POST https://app.weareoho.com/api/scan \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"type": "nesa",
"identifier": "123456",
"first_name": "Sarah",
"middle_name": "Jane",
"surname": "Smith",
"birth_date": "1990-05-15",
"constituent": {
"id": 12345
}
}'

Success Response:

{
"correlation_id": "d591aa45-64fa-46b9-8eab-423075ee66cb"
}

Linking to Existing Constituents

The API supports three workflows for managing NESA registrations:

When you want to associate the NESA registration with a person already in your system:

{
"type": "nesa",
"identifier": "123456",
"first_name": "Sarah",
"surname": "Smith",
"birth_date": "1990-05-15",
"constituent": {
"id": 12345
}
}

Behavior:

  • ✅ Links accreditation to constituent ID 12345
  • ✅ Accreditation appears in constituent's profile
  • ✅ Auto-updates constituent with additional details if fields are null
  • ❌ Returns 400 error if constituent ID doesn't exist in your organization

When to use:

  • You have a constituent database and want to track NESA registrations against people
  • You need to see all accreditations for a specific person
  • You want centralized compliance tracking

Scenario 2: Standalone Accreditation (No Constituent)

When you only want to validate credentials without linking to a person record:

{
"type": "nesa",
"identifier": "123456",
"first_name": "Sarah",
"surname": "Smith",
"birth_date": "1990-05-15"
}

Behavior:

  • ✅ Creates standalone accreditation record
  • ✅ Validates credentials against NESA register
  • ✅ Stores result in your organization
  • ⚠️ Not linked to any constituent profile
  • ⚠️ Can be linked later via the UI or API

When to use:

  • Quick one-off validations
  • You don't maintain a constituent database
  • Validating relief/casual teachers
  • Testing/development

Scenario 3: Invalid Constituent ID (Error)

When you provide a constituent ID that doesn't exist:

{
"type": "nesa",
"identifier": "123456",
"first_name": "Sarah",
"surname": "Smith",
"constituent": {
"id": 999999
}
}

Response (400 Bad Request):

{
"status": 400,
"message": "Validation error",
"errors": {
"constituent": {
"id": ["Constituent doesn't exist in your organization"]
}
}
}

How to avoid:

  1. Create the constituent first using POST /api/constituents
  2. Then submit the scan with the returned constituent ID
  3. Or omit constituent entirely for standalone accreditation

Webhook Configuration

Since the /api/scan endpoint processes requests asynchronously, results are delivered to your webhook endpoint when validation completes.

Webhook Setup Guide

For detailed instructions on setting up webhooks, configuring endpoints, and security best practices, see the Webhook Integration Guide.

Webhook Payload Structure

When a NESA scan completes, we POST the following JSON to your webhook:

Example 1: Valid Registration (Green)

{
"event": "accreditation_validation",
"correlation_id": "d591aa45-64fa-46b9-8eab-423075ee66cb",
"message_id": 54321,
"content": {
"notification_type": "accreditation-result",
"org_id": 123,
"previous": null,
"current": {
"id": 8765,
"identifier": "123456",
"type": "nesa",
"status": "active",
"status_color": "green",
"status_flags": [],
"registry_response": {
"response": ["Active"]
},
"meta": {
"status": {
"found": true,
"current": true,
"messages": []
}
}
},
"constituent": {
"id": 12345,
"first_name": "Sarah",
"surname": "Smith",
"email": "sarah.smith@example.com"
}
}
}

Example 2: Expired/Unconfirmed Registration (Yellow)

{
"event": "accreditation_validation",
"correlation_id": "e782cd67-86fc-68db-0gbc-645297gg88ed",
"message_id": 54322,
"content": {
"notification_type": "accreditation-result",
"org_id": 123,
"previous": null,
"current": {
"id": 8766,
"identifier": "234567",
"type": "nesa",
"status": "active",
"status_color": "yellow",
"status_flags": ["expired_unconfirmed"],
"registry_response": {
"response": ["Active"]
},
"meta": {
"status": {
"found": true,
"current": true,
"messages": []
}
}
},
"constituent": {
"id": 12346,
"first_name": "Michael",
"surname": "Wong",
"email": "michael.wong@example.com"
}
}
}

Example 3: Invalid/Not Found (Red)

{
"event": "accreditation_validation",
"correlation_id": "f893de78-97hd-80fd-2ide-867419ii00gf",
"message_id": 54323,
"content": {
"notification_type": "accreditation-result",
"org_id": 123,
"previous": null,
"current": {
"id": 8767,
"identifier": "345678",
"type": "nesa",
"status": "inactive",
"status_color": "red",
"status_flags": ["not_current"],
"registry_response": {
"response": ["Active"]
},
"meta": {
"status": {
"found": false,
"current": false,
"messages": []
}
}
},
"constituent": {
"id": 12347,
"first_name": "Jennifer",
"surname": "Lee",
"email": "jennifer.lee@example.com"
}
}
}

Note: The correlation_id in the webhook may differ from the one returned in the initial request. Use the identifier to match results.


Understanding the Response

Primary Indicators

Use these fields to determine registration validity:

  1. status_color - The primary indicator (check this first!)

    • "green" - Registration is valid and current
    • "yellow" - Registration needs attention (expired/unconfirmed)
    • "red" - Registration is invalid or not found
  2. status_flags - Array of specific conditions:

    • [] (empty) - No issues
    • ["expired_unconfirmed"] - Registration may be expired or unconfirmed
    • ["not_current"] - Registration is not current (details don't match or not found)
  3. meta.status - Validation details:

    • found - Whether the accreditation number was found in NESA register
    • current - Whether the details match and registration is current
    • messages - Any validation messages (usually empty)

Registry Response

Important: The registry_response object has limited information:

{
"response": ["Active"]
}

Fields:

  • response - Will show ["Active"] if found in NESA register

Critical: Do NOT rely on registry_response.response alone. NESA only indicates if a registration exists as "Active". The actual validation (matching teacher details) comes from status_color and meta.status.

Decision Logic

const statusColor = webhookData.content.current.status_color;
const statusFlags = webhookData.content.current.status_flags || [];
const metaStatus = webhookData.content.current.meta?.status;

if (statusColor === 'green' && statusFlags.length === 0) {
// ✅ Registration is valid and current
console.log('✅ Teacher registration is valid');

} else if (statusColor === 'yellow') {
// ⚠️ Needs attention
if (statusFlags.includes('expired_unconfirmed')) {
console.warn('⚠️ Registration may be expired or unconfirmed - review required');
}

} else if (statusColor === 'red') {
// ❌ Invalid or not found
if (statusFlags.includes('not_current')) {
if (!metaStatus?.found) {
console.error('❌ Registration not found in NESA register');
} else if (!metaStatus?.current) {
console.error('❌ Teacher details do not match NESA records');
}
}

return; // Do not approve for teaching
}

Best Practices

1. Always Check Status Color and Flags

Use status_color and status_flags as your primary validation:

const statusColor = webhookData.content.current.status_color;
const statusFlags = webhookData.content.current.status_flags || [];

if (statusColor === 'green' && statusFlags.length === 0) {
// Approved to teach
console.log('✅ Registration is valid - approved to teach');
} else if (statusColor === 'yellow') {
// Review required
console.warn('⚠️ Registration needs review - contact teacher');
} else if (statusColor === 'red') {
// Cannot teach
console.error('❌ Registration is not valid');
return;
}

2. Don't Rely on registry_response Alone

Wrong approach:

// ❌ DO NOT DO THIS
if (registry_response.response.includes('Active')) {
// This will always be true if found in register
// Does NOT mean the details are valid
}

Correct approach:

// ✅ DO THIS
if (status_color === 'green' && status_flags.length === 0) {
// Registration is validated and current
}

3. Handle Yellow Status Appropriately

Yellow status with expired_unconfirmed flag requires follow-up:

if (statusColor === 'yellow' && statusFlags.includes('expired_unconfirmed')) {
// Contact teacher to verify current registration status
await notifyTeacher({
teacher_id: constituent.id,
message: 'Please verify your NESA registration is current'
});

// Flag for manual review
await createReviewTask({
teacher_id: constituent.id,
reason: 'NESA registration may be expired or unconfirmed'
});
}

4. Prevent Duplicate Scans

Use the NESA accreditation number to track and prevent duplicates:

// Check if already scanned before submitting
const existing = await database.findByIdentifier('123456');
if (existing && existing.scanned_recently) {
// Skip scan or use cached result
return existing;
}

// Submit new scan
await submitNESAVerification({
type: 'nesa',
identifier: '123456',
first_name: 'Sarah',
surname: 'Smith'
});

5. Handle Webhooks Reliably

  • Return 200 OK quickly (within 10 seconds)
  • Process webhook data asynchronously in your application
  • Match webhooks using identifier (not correlation_id)
  • Store the accreditation.id for long-term references

Complete Integration Example

Step 1: Submit NESA Verification

curl -X POST https://app.weareoho.com/api/scan \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"type": "nesa",
"identifier": "123456",
"first_name": "Sarah",
"middle_name": "Jane",
"surname": "Smith",
"birth_date": "1990-05-15",
"constituent": {
"id": 12345
}
}'

Step 2: Receive Response

{
"correlation_id": "d591aa45-64fa-46b9-8eab-423075ee66cb"
}

Step 3: Store Request Details

// Store the NESA identifier and request details
await database.createVerificationRequest({
identifier: '123456',
constituent_id: 12345,
status: 'pending',
submitted_at: new Date()
});

Step 4: Receive Webhook (2-5 seconds later)

{
"event": "accreditation_validation",
"correlation_id": "e891df78-97gd-79ec-1hcd-756308hh99fe",
"content": {
"current": {
"id": 8765,
"identifier": "123456",
"type": "nesa",
"status": "active",
"status_color": "green",
"status_flags": [],
"registry_response": {
"response": ["Active"]
},
"meta": {
"status": {
"found": true,
"current": true,
"messages": []
}
}
},
"constituent": {
"id": 12345,
"first_name": "Sarah",
"surname": "Smith"
}
}
}

Note: The correlation_id in the webhook may differ from the one returned in Step 2. Use the identifier to match results.

Step 5: Process Webhook

async function handleNESAWebhook(req, res) {
// 1. Verify signature
const signature = req.headers['x-hub-signature'];
if (!verifyWebhook(req.body, signature, process.env.OHO_SECRET)) {
return res.status(403).send('Invalid signature');
}

const payload = JSON.parse(req.body);

// 2. Match to your records using the identifier
const identifier = payload.content.current.identifier;
const verificationRequest = await database.findByIdentifier(identifier);

// 3. Check registration validity using status_color and flags
const statusColor = payload.content.current.status_color;
const statusFlags = payload.content.current.status_flags || [];
const metaStatus = payload.content.current.meta?.status;

const isValid = statusColor === 'green' && statusFlags.length === 0;

// 4. Update your records
await database.updateVerificationRequest(verificationRequest.id, {
status: payload.content.current.status,
status_color: statusColor,
status_flags: statusFlags,
accreditation_id: payload.content.current.id,
meta_found: metaStatus?.found || false,
meta_current: metaStatus?.current || false,
is_valid: isValid
});

// 5. Respond quickly
res.status(200).send('OK');

// 6. Process asynchronously (after response sent)
if (statusColor === 'yellow' && statusFlags.includes('expired_unconfirmed')) {
await notifyComplianceTeam({
teacher: verificationRequest,
reason: 'NESA registration may be expired or unconfirmed'
});
}

if (statusColor === 'red') {
await createAlert({
teacher: verificationRequest,
severity: 'high',
message: 'NESA registration is not valid - teacher cannot work'
});
}
}

Common Use Cases

Education Onboarding

Verify NESA registration during teacher onboarding:

async function onboardTeacher(teacherData) {
// 1. Create constituent
const constituent = await createConstituent({
first_name: teacherData.firstName,
surname: teacherData.surname,
email: teacherData.email,
mobile_number: teacherData.mobile
});

// 2. Submit NESA verification
const nesaCheck = await submitNESAVerification({
type: 'nesa',
identifier: teacherData.nesaNumber,
first_name: teacherData.firstName,
surname: teacherData.surname,
birth_date: teacherData.dob,
constituent: { id: constituent.id }
});

return {
constituent_id: constituent.id,
correlation_id: nesaCheck.correlation_id
};
}

Bulk Teacher Verification

Verify multiple teachers simultaneously:

async function bulkVerifyTeachers(teachers) {
const results = [];

for (const teacher of teachers) {
const response = await submitNESAVerification({
type: 'nesa',
identifier: teacher.nesaNumber,
first_name: teacher.firstName,
surname: teacher.surname,
birth_date: teacher.dob,
constituent: { id: teacher.constituentId }
});

results.push({
teacher_id: teacher.id,
correlation_id: response.correlation_id
});

// Rate limiting: wait 100ms between requests
await sleep(100);
}

return results;
}

Periodic Re-verification

Set up periodic checks to monitor ongoing registration status:

async function schedulePeriodicReVerification(constituentId, nesaNumber) {
// Check every 90 days
setInterval(async () => {
await submitNESAVerification({
type: 'nesa',
identifier: nesaNumber,
first_name: constituent.first_name,
surname: constituent.surname,
constituent: { id: constituentId }
});
}, 90 * 24 * 60 * 60 * 1000); // 90 days
}

Error Responses

Invalid Constituent (400)

{
"status": 400,
"message": "Validation error",
"errors": {
"constituent": {
"id": ["Constituent doesn't exist in your organization"]
}
}
}

Authentication Error (401)

{
"status": 401,
"message": "You are not authorized to view this resource",
"field": "authentication"
}

Service Unavailable (503)

{
"status": 503,
"message": "The target resource is currently unavailable"
}

FAQ

Q: How often should I verify NESA registrations?

A:

  • Initial verification: Always verify before employment commences
  • Ongoing monitoring: Quarterly or bi-annual checks recommended
  • Annual verification: Before each school year begins
  • Ad-hoc verification: After any reported concerns

Q: What does "expired_unconfirmed" mean?

A: This flag indicates the registration may be expired or Oho cannot confirm current status. This requires manual follow-up with the teacher to verify their current NESA registration status.

Q: Why does registry_response always show "Active"?

A: NESA's registry only indicates if an accreditation number exists as "Active" in their system. It doesn't validate if the teacher's details match. Oho performs additional validation by matching the teacher's name and details, which is reflected in status_color and meta.status.

Q: What should I do if status is red with "not_current" flag?

A: This means either:

  1. The accreditation number was not found in NESA register (meta.status.found: false), or
  2. The teacher's details (name, DOB) don't match NESA records (meta.status.current: false)

The teacher cannot legally teach in NSW schools. Verify the accreditation number and details with the teacher.

Q: Can I verify teachers from other states?

A: No, NESA only covers NSW teachers. For teachers in other states, use the appropriate state teacher registration verification:

  • VIC: VIT (Victorian Institute of Teaching)
  • QLD: QCT (Queensland College of Teachers)
  • Other states have their own registration authorities

Q: What if I submit the same NESA check twice?

A: Each request creates a new scan. To prevent duplicates, check your database for existing scans of the same NESA number before submitting. Track scans using the identifier field (NESA accreditation number) rather than relying on correlation_id.


Troubleshooting

Common Issues and Solutions

1. 401 Unauthorized Error

Problem: {"status": 401, "message": "You are not authorized to view this resource"}

Solutions:

  • ✅ Verify your API token is correct and hasn't expired
  • ✅ Check the Authorization header format: Bearer YOUR_TOKEN
  • ✅ Ensure your user account is verified (check email)
  • ✅ Confirm your organization is active and in good standing

2. Registration Not Found (Red Status)

Problem: Webhook returns status_color: "red" with not_current flag and meta.status.found: false

Solutions:

  • ✅ Verify the NESA accreditation number is correct
  • ✅ Ask the teacher to check their NESA registration certificate
  • ✅ Confirm the teacher is registered in NSW (not another state)
  • ✅ Check for typos in the accreditation number

3. Details Mismatch (Red Status)

Problem: Webhook returns status_color: "red" with not_current flag but meta.status.found: true

Solutions:

  • ✅ Verify the teacher's name spelling matches their NESA registration exactly
  • ✅ Check for name changes (marriage, etc.)
  • ✅ Include middle name if it appears on NESA registration
  • ✅ Ask teacher to verify their details in NESA portal

4. Yellow Status with expired_unconfirmed

Problem: Webhook shows status_color: "yellow" with expired_unconfirmed flag

Solutions:

  • ✅ Contact the teacher to verify their current NESA registration status
  • ✅ Ask teacher to log in to NESA portal and confirm registration is current
  • ✅ Teacher may need to renew their registration
  • ✅ Do not approve for teaching until status is confirmed

Quick Reference

Endpoint Summary

Endpoint: POST /api/scan

Content-Type: application/json

Authentication: Authorization: Bearer YOUR_TOKEN

Type Code: nesa

Field Requirements

Always required:

  • type - Must be "nesa"
  • identifier - NESA accreditation number
  • first_name - First/given name
  • surname - Surname/family name

Optional:

  • middle_name - Middle name(s)
  • birth_date - Date of birth (YYYY-MM-DD)
  • constituent.id - Link to existing constituent

Status Color Values (Primary Indicator)

ColorDescriptionAction Required
greenRegistration is valid✅ Approved to teach
yellowNeeds attention (expired/unconfirmed)⚠️ Review and verify
redInvalid or not found❌ Cannot teach

Status Flags

FlagMeaningAction Required
Empty []No issues✅ Valid registration
expired_unconfirmedMay be expired or unconfirmed⚠️ Contact teacher to verify
not_currentNot found or details don't match❌ Cannot teach

Meta Status Fields

FieldDescriptionWhen to Check
meta.status.foundWhether accreditation number found in NESA registerWhen status_color: "red"
meta.status.currentWhether teacher details match NESA recordsWhen status_color: "red"

When red status: If meta.status.found: false, the number wasn't found. If meta.status.current: false, the details don't match.



External Resources


Document Version

Version: 1.0 Last Updated: 2026-02-06 Endpoint: POST /api/scan (Universal with type: "nesa") Status: Production Ready