NESA Teacher Registration Verification
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.responsewill always show["Active"]if found in the NESA register- The actual validation result comes from
status_colorandstatus_flags - Oho validates by checking if the teacher details match NESA records
Status Interpretation
| Status Color | Meaning | status_flags | meta.status |
|---|---|---|---|
green | Valid and current | Empty {} | found: true, current: true |
yellow | Needs attention | expired_unconfirmed | found: true, current: true |
red | Invalid/Not found | not_current | found: 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:
- Active organization account - Your organization must be active and in good standing
- Verified user status - Your API user must have completed email verification
- 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
| Field | Type | Description | Required |
|---|---|---|---|
type | string | Must be "nesa" | Yes |
identifier | string | NESA accreditation number | Yes |
first_name | string | First/given name | Yes |
middle_name | string | Middle name(s) | No |
surname | string | Surname/family name | Yes |
birth_date | string (YYYY-MM-DD) | Date of birth | No |
constituent.id | integer | Link to existing constituent | No |
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:
- On their NESA registration certificate
- In the NESA portal
- 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:
- Store the
identifier(NESA number) with your internal records when submitting a scan - When you receive a webhook, match it using
content.current.identifier - 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:
Scenario 1: Link to Existing Constituent (Recommended)
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
400error 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:
- Create the constituent first using
POST /api/constituents - Then submit the scan with the returned constituent ID
- Or omit
constituententirely for standalone accreditation
Webhook Configuration
Since the /api/scan endpoint processes requests asynchronously, results are delivered to your webhook endpoint when validation completes.
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:
-
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
-
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)
-
meta.status- Validation details:found- Whether the accreditation number was found in NESA registercurrent- Whether the details match and registration is currentmessages- 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 OKquickly (within 10 seconds) - Process webhook data asynchronously in your application
- Match webhooks using
identifier(not correlation_id) - Store the
accreditation.idfor 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:
- The accreditation number was not found in NESA register (
meta.status.found: false), or - 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
Authorizationheader 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 numberfirst_name- First/given namesurname- 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)
| Color | Description | Action Required |
|---|---|---|
green | Registration is valid | ✅ Approved to teach |
yellow | Needs attention (expired/unconfirmed) | ⚠️ Review and verify |
red | Invalid or not found | ❌ Cannot teach |
Status Flags
| Flag | Meaning | Action Required |
|---|---|---|
Empty [] | No issues | ✅ Valid registration |
expired_unconfirmed | May be expired or unconfirmed | ⚠️ Contact teacher to verify |
not_current | Not found or details don't match | ❌ Cannot teach |
Meta Status Fields
| Field | Description | When to Check |
|---|---|---|
meta.status.found | Whether accreditation number found in NESA register | When status_color: "red" |
meta.status.current | Whether teacher details match NESA records | When 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.
Related Documentation
- WWC Integration Guide - Working With Children Check verification
- AHPRA Integration Guide - Health practitioner registration
- Constituent Management Guide - Managing people in your organization
- Webhook Integration Guide - Setting up webhooks for all check types
- Glossary - Comprehensive terminology reference
External Resources
- NESA Website - NSW Education Standards Authority
- NESA Teacher Portal - Teacher login portal
- NESA Registration Requirements - Registration information
Document Version
Version: 1.0
Last Updated: 2026-02-06
Endpoint: POST /api/scan (Universal with type: "nesa")
Status: Production Ready