Overview
The Education verification API lets you confirm that an applicant to your education program is associated with a recognised educational institution. The same flow supports both student verification (typically higher education – universities, colleges, community colleges) and teacher verification (typically K–12 – primary and secondary schools, with some higher education).
If you would prefer Goodstack to host the UI for your application flow, please get in touch with your account manager about a hosted setup for Education verification.
Below we outline a typical API implementation. Code examples are shown for both audiences – toggle the Student / Teacher tabs in each example to see the relevant payload.
1. Get your API keys
Get in touch with our product & engineering team to get access to the dashboard: engineering-support@goodstack.io
Before you start your implementation, you will need access to the dashboard to retrieve your Goodstack API keys.
Go to the Keys page in your Partner dashboard to access your Publishable Key and your Secret Key.
Authenticate your API requests by using these keys in the Authorization request header. The secret key should be used for server-to-server requests; the publishable key is for requests made from your front end to public API endpoints.
Do not publish your secret key in source control or use it in any front-end code.
2. Find the applicant's institution
2a. Collect the country
We strongly recommend collecting the applicant's country as the first step of your flow. Including a countryCode when you search for institutions makes results dramatically more accurate.
You can retrieve the full list of supported countries (with their ISO three-letter codes) from the Countries API:
GET /v1/countries
200 OK
{
"data": [
{
"code": "GBR",
"name": "United Kingdom"
}
],
"object": "Country"
}
Country codes are three-letter ISO codes (USA, GBR, FRA, etc.).
2b. Search for the institution
Call the Search Organisations endpoint with the applicant's countryCode, type[]=education, and a search query. The type[]=education filter restricts results to schools, colleges, universities, and other recognised educational institutions in Goodstack's database. This endpoint can be authenticated with your publishable key.
- Student
- Teacher
GET /v1/organisations?type[]=education&countryCode=USA&query=greenfield
200 OK
{
"data": [
{
"id": "organisation_xxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"name": "Greenfield University",
"countryCode": "USA"
}
]
}
GET /v1/organisations?type[]=education&countryCode=GBR&query=westbridge
200 OK
{
"data": [
{
"id": "organisation_xxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"name": "Westbridge Primary School",
"countryCode": "GBR"
}
]
}
Path A – Institution found
If the applicant selects their institution from the search results, store the returned organisationId for the Validation Submission request in step 4.
Path B – Institution not found
If no matching institution appears, switch your form to manual entry. Collect the following fields from the applicant:
organisationName– the name of the school (required)countryCode– three-letter ISO code (required)website– the school's official website (optional but highly recommended; a website helps our verification team locate the institution faster)addressLine1,addressLine2,city,state,postal– the school's address (optional but recommended; they help our verification team locate the institution faster)
The submission also requires a language field – this is collected with the applicant's details in §3.
You may notice that the general Create Validation Submission reference lists registryName and registryId as required fields. These are not required for Education verification and you should not ask the applicant for them as commonly students and teachers do not know their institution's registry identifier or it may not apply.
Goodstack relaxes the registry requirement whenever your configuration's allowed organisation types include education (whether on its own or alongside nonprofit / social_impact). Omit these fields from your manual-entry payload and the submission will be accepted.
3. Collect applicant details
Collect the applicant's name, email, and preferred language. These are sent with the Validation Submission and are used by the Agent Verification check to confirm the applicant is associated with the institution (for example, by checking that their email domain matches the school).
- Student
- Teacher
{
"firstName": "Alex",
"lastName": "Doe",
"email": "alex.doe@greenfield.edu",
"language": "en-US"
}
{
"firstName": "Jane",
"lastName": "Smith",
"email": "jane.smith@westbridge.sch.uk",
"language": "en-GB"
}
The language field accepts any RFC 5646 language code (en-US, en-GB, fr-FR, etc.).
4. Submit the validation
All verification checks are asynchronous. Most submissions resolve within a few seconds, but some can take up to 72 hours.
Once you have the institution and applicant details, submit them to the Create Validation Submission endpoint.
Validation configuration
Each submission must reference a configurationId that tells Goodstack which checks to run and which institution types qualify. Configurations are set up by our onboarding team and your account manager will provide your configurationId before you go live. You can list the configurations active on your account via the Retrieve Partner Configurations API.
What runs behind the scenes
Each Education verification submission runs three checks automatically – you do not need to call separate endpoints to create them:
- Validation Request – confirms the institution exists and is legitimate
- Eligibility Check – determines the institution type (e.g. university, college, K–12)
- Agent Verification – confirms the applicant is associated with the institution
Submission payload
Variant A – with organisationId. When the applicant found their institution in search results (Path A from §2):
- Student
- Teacher
POST https://api.goodstack.io/v1/validation-submissions
Authorization: Secret Key
Content-Type: application/json
{
"configurationId": "configuration_xxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"organisationId": "organisation_xxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"firstName": "Alex",
"lastName": "Doe",
"email": "alex.doe@greenfield.edu",
"language": "en-US"
}
POST https://api.goodstack.io/v1/validation-submissions
Authorization: Secret Key
Content-Type: application/json
{
"configurationId": "configuration_xxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"organisationId": "organisation_xxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"firstName": "Jane",
"lastName": "Smith",
"email": "jane.smith@westbridge.sch.uk",
"language": "en-GB"
}
Variant B – without organisationId. When the applicant entered their institution details manually (Path B from §2). registryName and registryId are omitted – see the callout in §2:
- Student
- Teacher
POST https://api.goodstack.io/v1/validation-submissions
Authorization: Secret Key
Content-Type: application/json
{
"configurationId": "configuration_xxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"organisationName": "Greenfield University",
"website": "https://www.greenfield.edu",
"addressLine1": "500 College Avenue",
"city": "San Jose",
"state": "California",
"postal": "95112",
"countryCode": "USA",
"firstName": "Alex",
"lastName": "Doe",
"email": "alex.doe@greenfield.edu",
"language": "en-US"
}
POST https://api.goodstack.io/v1/validation-submissions
Authorization: Secret Key
Content-Type: application/json
{
"configurationId": "configuration_xxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"organisationName": "Westbridge Primary School",
"website": "https://westbridge.sch.uk",
"addressLine1": "12 High Street",
"city": "Westbridge",
"state": "England",
"postal": "WB1 2CD",
"countryCode": "GBR",
"firstName": "Jane",
"lastName": "Smith",
"email": "jane.smith@westbridge.sch.uk",
"language": "en-GB"
}
Submissions created via Variant B have a null organisationId on creation. If the institution details are confirmed valid, Goodstack will create the organisation record and the ID will be available via webhook or the Retrieve Validation Submission API.
Adding metadata
The submission also accepts an optional metadata object – an arbitrary key-value record you can use to link the submission back to your own systems (account IDs, application form fields, etc.). Metadata is returned with the submission on every read and in every webhook.
5. Interpret the response
When you submit a validation, Goodstack responds with a 200 OK and the current state of the submission. The top-level status tells you whether the application is complete, in review, or needs further input from the applicant.
The status field will be one of:
| Status | Meaning |
|---|---|
succeeded | All checks passed. The applicant qualifies. |
failed | One or more checks failed. The applicant does not qualify. |
pending | Checks are still running, or the applicant needs to provide more information. |
When status === 'pending', look at the nested agentVerification.status to decide whether you need to ask the applicant for documentation:
agentVerification.status | Meaning | UI action |
|---|---|---|
approved | Agent check passed; other checks still running | Show "in review" screen |
pending_user_verification | Goodstack is reaching out to the applicant directly | Show "in review" screen |
pending_review | Submission queued for manual review by Goodstack | Show "in review" screen |
pending | Goodstack needs additional documentation to verify the applicant | Prompt the applicant to upload a document (see §6) |
rejected | Agent verification failed | Show failure messaging (see §7 for failure reasons) |
The decision tree your front end should implement:
Case 1 – Application fully approved
{
"status": "succeeded",
"agentVerification": { "status": "approved" }
}
Show the success screen. No further applicant action is required.
Case 2 – In review, no applicant action needed
{
"status": "pending",
"agentVerification": { "status": "approved" }
}
Show an "Application is being reviewed" screen. The applicant does not need to do anything; you will receive a webhook when the submission resolves.
Case 3 – Documentation required
{
"status": "pending",
"agentVerification": { "status": "pending" }
}
OR
{
"status": "pending",
"agentVerification": { "status": "pending_review" }
}
Prompt the applicant to upload supporting documentation. See §6.
6. Upload supporting documents
When the applicant needs to upload documentation, send it to the Create Validation Submission Document endpoint as a multipart/form-data request:
POST https://api.goodstack.io/v1/validation-submission-documents
Authorization: Secret Key
Content-Type: multipart/form-data
file=<binary>
validationSubmissionId=validationsubmission_xxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Accepted formats: jpg, png, pdf. Maximum file size: 5 MB.
Once uploaded, the document is reviewed by Goodstack's verification team. You will receive a validation_submission.succeeded or validation_submission.failed webhook (see §7) once the review is complete.
The document the applicant uploads has a large impact on how quickly the verification completes. Give the applicant a short, specific list of acceptable proofs rather than a generic "upload a document" prompt. Examples include a student ID, an enrolment letter, an official school correspondence, or – for teachers – a payslip showing the school name or a letter on school letterhead.
7. Receive and process results (webhooks)
To receive results, you will need the Webhook Subscriptions API.
After creating validation submissions, subscribe to webhooks so your system is notified when each submission resolves. The three events for Education verification are:
validation_submission.created– fired as soon as you successfully POST to the create endpointvalidation_submission.succeeded– all checks in the configuration passedvalidation_submission.failed– one or more checks failed
Example payloads
validation_submission.created – sent when Goodstack creates a Validation Submission.
{
"object": "event",
"data": {
"id": "event_xxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"eventType": "validation_submission.created",
"createdAt": "2026-04-15T11:00:00.000Z",
"eventData": {
"id": "validationsubmission_xxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"status": "pending",
"createdAt": "2026-04-15T10:30:00.000Z",
"organisationId": "organisation_xxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"agentVerificationId": "agentverification_xxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"agentVerification": {
"id": "agentverification_xxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"firstName": "Alex",
"lastName": "Doe",
"email": "alex.doe@greenfield.edu",
"status": "pending",
"rejectionReasonCode": null
},
"eligibilitySubscriptionId": "eligibilitysubscription_xxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"validationRequestId": "validationrequest_xxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"validationRequest": {
"id": "validationrequest_xxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"name": "Greenfield University",
"acceptedAt": null,
"rejectedAt": null,
"organisationTypes": []
},
"monitoringSubscriptionId": null,
"validationInviteId": null,
"partnerFields": {},
"metadata": {}
}
}
}
validation_submission.failed – sent when a submission moves to the failed state. The failureReasons array tells you which checks failed and why.
{
"object": "event",
"data": {
"id": "event_xxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"eventType": "validation_submission.failed",
"createdAt": "2026-04-15T11:10:00.000Z",
"eventData": {
"id": "validationsubmission_xxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"status": "failed",
"agentVerification": {
"status": "rejected",
"rejectionReasonCode": "fake_email_used"
},
"failureReasons": [
{
"check": "agent_verification",
"reason": { "rejectionReasonCode": "fake_email_used" }
},
{
"check": "eligibility",
"reason": {
"status": "live",
"results": {
"eligibilityStatus": "fail"
}
}
}
]
}
}
}
The validation_submission.succeeded payload has the same shape with status: "succeeded" and no failureReasons array.
Handling common failure reasons
When a submission fails, the failureReasons array breaks down which checks failed and gives a machine-readable reason. The most common reasons your front end should handle:
| Rejection Reason Code | What it means | What to surface to the applicant |
|---|---|---|
fake_email_used | The applicant's email could not be verified as belonging to the institution | "We could not verify your school email. Please reapply using a valid school-issued address." |
validation_request_failed | We could not confirm the institution is a legitimate educational organisation | "We could not verify your school. Please double-check the school details or contact support." |
user_verification_expired | The applicant did not respond to a verification step in time | "Your application has expired. Please reapply." |
other | A reason that does not map to a specific code (review the webhook payload) | Generic failure messaging; consider logging for review |
Always check the full failureReasons array – a submission can fail for more than one reason.
Subscribing and verifying webhooks
Before subscribing, create server-side event handlers to trigger any required actions (account upgrades, follow-up emails, etc.). When your handlers are ready, register them via the Create Webhook Subscription endpoint.
Process webhooks as quickly as possible and return a 200 response as soon as you have verified and stored the event. If your handler is slow, Goodstack will time out and retry. Use a queue if your processing is non-trivial.
You can verify that an incoming webhook was sent by Goodstack using the standard signature-verification pattern. See the Verifying webhooks section of the Webhooks concept page for a complete code example and details on the Goodstack-Signature header.
Flow diagram
End-to-end flow of an Education verification submission from the applicant's perspective: