Skip to main content

Education verification: API

Overview

The Education Verification API confirms two things in one workflow: that an institution is a legitimate school, and that your applicant is a student or teacher there. Use it to gate education discounts and offers.

Who you can verify

  • Students – focuses on higher education (universities, colleges, and community colleges).
  • Teachers – primarily covers K–12 (primary and secondary schools), with select higher education coverage.

How it works

This guide walks you through a direct API integration, step by step. The code examples cover both segments – use the Student / Teacher tabs in the code blocks to see the payload for your use case.

End to end, an Education verification looks like this from the applicant's perspective:

info

Prefer a low-code option? If you don't want to build the front-end UI yourself, Goodstack can host the verification flow for you. Contact your account manager to get started.

1. Choose your integration path

You can integrate the Education Verification API in two ways. Both run the same verification checks – the difference is which of your systems talks to Goodstack, and whether applicant documents pass through your infrastructure.

Server-sideDirect browser
Who calls GoodstackYour backend, with your secret keyYour backend creates a validation invite; your front end then calls Goodstack directly with your publishable key
Document handlingYour backend receives files and forwards them to GoodstackYour front end sends files directly to Goodstack – they never touch your servers
Document timingUpload after the submission exists, when your configuration calls for documentationUpload up-front, before the submission is created
ConfigurationA configurationId, passed with each submissionA hostedConfigId, passed when creating each validation invite
Best forThe simplest integration, when handling documents on your servers is acceptablePrograms that must avoid receiving, storing, or logging applicant documents (privacy / compliance requirements)

After the shared setup, choose the path that matches how you want to handle applicant documents:

  • Server-side integration – your backend sends the submission and any supporting documents to Goodstack using your secret key.
  • Direct browser integration – your front end sends documents and the submission directly to Goodstack, after your backend creates a validation invite.

2. Get your API keys

info

Get in touch with our product & engineering team to get access to the dashboard: engineering-support@goodstack.io

Before you start, you need access to the dashboard to retrieve your Goodstack API keys.

Go to the API Keys tab in your Partner dashboard settings to access your Publishable Key and your Secret Key.

Authenticate your API requests by passing a key in the Authorization request header. Use the secret key for server-to-server requests, and the publishable key for requests your front end makes to public API endpoints.

Pass the key as the raw header value, like this:

Authorization: <secret-key>

Never publish your secret key in source control or use it in front-end code.

3. Find the applicant's institution

3a. Collect the country

Collect the applicant's country as the first step of your flow. Passing a countryCode when you search for institutions makes results much more accurate, so we strongly recommend it.

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.).

3b. 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. You can authenticate this endpoint with your publishable key.

GET /v1/organisations?type[]=education&countryCode=USA&query=greenfield

200 OK
{
"data": [
{
"id": "organisation_xxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"name": "Greenfield University",
"countryCode": "USA"
}
]
}

3c. Filter by education stage (optional)

If your program only applies to certain education levels – for example a teacher program limited to K–12 – you can narrow search results by education stage. Goodstack tags each institution in its database with the stages it teaches:

TagStage
gs.stage:pre-primaryPre-primary (nursery, kindergarten)
gs.stage:primaryPrimary education
gs.stage:secondarySecondary education
gs.stage:higherHigher education (universities, colleges)

An institution can carry more than one stage tag – a school that spans kindergarten through primary has both gs.stage:pre-primary and gs.stage:primary.

Pass the stages you want as a comma-separated list in the tags[] parameter. A comma-separated list matches institutions with any of the listed tags:

GET /v1/organisations?type[]=education&countryCode=USA&query=greenfield&tags[]=gs.stage:higher

Returns only higher-education institutions.

Prefer tags[] over notTags[]

The endpoint also supports a notTags[] exclusion parameter, but for stage filtering an include list is safer. notTags[] excludes an institution if it carries any of the listed tags – so excluding gs.stage:pre-primary from a K–12 search would also drop a school that teaches both pre-primary and primary, even though it qualifies. With tags[], that school is correctly included.

Repeating the parameter (tags[]=X&tags[]=Y) requires all listed tags instead – see the Search Organisations reference for the full matching rules.

3d. Handle found and manual institutions

If the applicant selects their institution from the search results, store the returned organisationId for the Validation Submission request.

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)
  • addressLine1, addressLine2, city, state, postal – the school's address (optional but recommended)

A website and address help our verification team locate the institution faster.

The submission also requires a language field – you collect this with the applicant's details in step 4.

Registry fields not required for Education

The general Create Validation Submission reference lists registryName and registryId as required fields. Education verification does not require them, and you should not ask the applicant for them – students and teachers rarely know their institution's registry identifier, and it often does not apply.

Goodstack eliminates 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 Goodstack accepts the submission.

4. Collect applicant details

Collect the applicant's name, email, and preferred language. You send these with the Validation Submission, and the Agent Verification check uses them to confirm the applicant is associated with the institution (for example, by checking that their email domain matches the school).

{
"firstName": "Alex",
"lastName": "Doe",
"email": "alex.doe@greenfield.edu",
"language": "en-US"
}

The language field accepts any RFC 5646 language code (en-US, en-GB, fr-FR, etc.).

Each submission runs three checks automatically – you don't call separate endpoints to create them:

info

All verification checks run asynchronously. Many submissions resolve within seconds to a couple hours, however in some cases it may take up to 72 hours.

5. Server-side integration

Choose this path when your backend can receive applicant documents and forward them to Goodstack. Your backend makes all write calls to Goodstack with your secret key.

5a. Create the validation submission

Send the institution and applicant details to the Create Validation Submission endpoint. Reference your validation configuration with configurationId.

Our onboarding team sets up configurations, and your account manager provides your configurationId before you go live. You can list the configurations active on your account via the Retrieve Partner Configurations API.

With organisationId

Use this payload when the applicant selected their institution from search results:

POST https://api.goodstack.io/v1/validation-submissions
Authorization: <secret-key>
Idempotency-Key: ver_attempt_01HZR9YJ6K8X4R7Q2N5P3M1A0B_submission
Content-Type: application/json

{
"configurationId": "configuration_xxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"organisationId": "organisation_xxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"firstName": "Alex",
"lastName": "Doe",
"email": "alex.doe@greenfield.edu",
"language": "en-US"
}

With manual institution details

Use this payload when the applicant entered institution details manually. Omit registryName and registryId – see Registry fields not required for Education.

POST https://api.goodstack.io/v1/validation-submissions
Authorization: <secret-key>
Idempotency-Key: ver_attempt_01HZR9YJ6K8X4R7Q2N5P3M1A0B_submission
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"
}

Submissions created with manual institution details have a null organisationId on creation. If our verification team confirms the institution details, Goodstack creates the organisation record and exposes its ID via webhook and the Retrieve Validation Submission API.

5b. Store the submission response

Goodstack responds with 200 OK and the submission. Store the IDs you'll need later – at minimum the submission id, and agentVerification.id if you support document upload:

{
"data": {
"id": "validationsubmission_xxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"status": "pending",
"createdAt": "2026-04-15T10:30:00.000Z",
"organisationId": "organisation_xxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"organisationName": "Greenfield University",
"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",
"validationInviteId": null,
"metadata": {},
"monitoring": { "status": null, "results": null },
"eligibility": { "status": "live", "results": null }
},
"object": "ValidationSubmission"
}

The submission also accepts an optional metadata object – an arbitrary key-value record that links the submission back to your own systems (account IDs, application form fields, etc.). Goodstack returns metadata with the submission on every read and in every webhook.

Protect against duplicate submissions

Create one verification attempt in your system before calling Goodstack, and use a stable Idempotency-Key for the create-submission request. If the request times out, retry with the same idempotency key and the same payload. After Goodstack responds, store the returned data.id against the attempt and reuse that submission if the applicant refreshes. You can also include your attempt ID in metadata so reads and webhooks are easier to reconcile.

5c. Show the applicant the current state

Drive your front end off the top-level status. The create-submission response needs only three screens:

statusUI action
succeededShow the success screen – the applicant is done.
failedShow failure messaging (see failure reasons).
pendingShow an "Application is being reviewed" screen, or prompt for a document if your configuration requires one.

During your implementation, Goodstack will consult with you on your configuration and let you know whether your checks require documents. If they do, prompt for them when status is pending, regardless of the nested agent verification status. If your configuration doesn't require documents, you don't need to prompt for them at all.

5d. Upload supporting documents, if required

When your configuration calls for documentation, your front end collects the file, and your backend receives it and forwards it to Goodstack with the submission ID.

Your backend makes this call and authenticates with your secret key:

POST https://api.goodstack.io/v1/validation-submission-documents
Authorization: <secret-key>
Idempotency-Key: ver_attempt_01HZR9YJ6K8X4R7Q2N5P3M1A0B_document_1
Content-Type: multipart/form-data

validationSubmissionId=validationsubmission_xxxxxxxxxxxxxxxxxxxxxxxxxxxxx
file=<enrolment-letter.pdf>

Use a unique Idempotency-Key for each document upload, and reuse it only when retrying the same file upload.

Once the document is uploaded, Goodstack's verification team reviews it. You receive a validation_submission.succeeded or validation_submission.failed webhook once the review is complete.

Next, see Shared reference for status meanings, document rules, webhook events, and failure reasons.

6. Direct browser integration

Choose this path when applicant documents should go straight from the browser to Goodstack. Your backend still creates the session, but document bytes never touch your servers.

In the direct browser flow:

  • Your backend creates a validation invite with your secret key.
  • Your front end uploads any applicant document with your publishable key and validationInviteId.
  • Your front end creates the validation submission with your publishable key and the same validationInviteId.
  • Your backend receives the final result by webhook.

6a. Create a validation invite

Your backend starts each verification session by creating a validation invite with the Create Validation Invite endpoint. The invite is a browser-safe correlation token: it lets your front end upload documents and create the submission directly against Goodstack with your publishable key, while your secret key stays on your server and document bytes stay out of your infrastructure.

POST https://api.goodstack.io/v1/validation-invites
Authorization: <secret-key>
Content-Type: application/json

{
"hostedConfigId": "hostedconfiguration_xxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"language": "en-US",
"metadata": {
"applicantId": "user_123"
}
}

The hostedConfigId references the hosted configuration that defines which checks run and which institution types qualify. It plays the same role as the configurationId in the server-side integration. You receive it during onboarding.

Goodstack responds with a validationInviteId. Return only the validationInviteId to your front end – it is safe to expose in the browser.

Keep applicants in your own UI

The invite response may include a hosted form URL because validation invites can also be used with Goodstack-hosted forms. In this direct browser flow, you do not need to redirect applicants. Pass the validationInviteId to your browser so it can upload documents and create the submission.

6b. Upload applicant documents up-front

Your front end uploads the file straight to Goodstack before creating the submission, using the validationInviteId. Send the request as multipart/form-data.

POST https://api.goodstack.io/v1/validation-submission-documents
Authorization: <publishable-key>
Idempotency-Key: ver_attempt_01HZR9YJ6K8X4R7Q2N5P3M1A0B_document_1
Content-Type: multipart/form-data

validationInviteId=validationinvite_xxxxxxxxxxxxxxxxxxxxxxxxxxxxx
documentType=agent_verification
file=<student-id.jpg>

Multipart fields:

FieldRequiredDescription
validationInviteIdYesThe invite ID your backend created in step 6a
fileYesThe document the applicant selected
documentTypeNovalidation_request (default) for evidence about the institution; agent_verification for evidence about the applicant (e.g. a student ID or payslip)

For an education flow, most applicant-uploaded documents are evidence about them – such as a student ID, enrolment letter, school email evidence, teacher payslip, or letter from the school – so use agent_verification. Use validation_request only for evidence about the institution itself.

Collect documents up-front

Collecting documentation up-front is the recommended direct browser path and gives the highest verification success rates – the document is already attached when the checks run, so you never have to bring the applicant back into the flow to provide more evidence.

6c. Create the validation submission

After any document uploads are complete, your front end creates the submission directly with the Create Validation Submission endpoint. Use the publishable key and pass the validationInviteId instead of configurationId.

If the applicant selected an institution from search results, pass organisationId:

POST https://api.goodstack.io/v1/validation-submissions
Authorization: <publishable-key>
Idempotency-Key: ver_attempt_01HZR9YJ6K8X4R7Q2N5P3M1A0B_submission
Content-Type: application/json

{
"validationInviteId": "validationinvite_xxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"organisationId": "organisation_xxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"firstName": "Alex",
"lastName": "Doe",
"email": "alex.doe@greenfield.edu",
"language": "en-US",
"metadata": {
"applicantId": "user_123"
}
}

If the applicant entered institution details manually, replace organisationId with the manual institution fields from step 3d. The applicant fields are otherwise identical.

Creating the submission completes the invite: Goodstack attaches any documents already uploaded against the validationInviteId to the new submission, and you cannot reuse the invite afterwards.

Goodstack responds with the same submission shape as the server-side integration. In the direct browser flow, validationInviteId is populated with the invite the submission was created from, and metadata echoes back what you sent.

Metadata is the recommended way to match direct browser submissions to your own user records. Goodstack returns metadata with the submission on every read and in every webhook.

Treat each invite as one verification attempt

Store the validationInviteId against your applicant session before returning it to the browser, and prevent the browser from submitting the same invite more than once. Use a stable Idempotency-Key when the browser creates the submission, and retry with the same key and payload if the browser does not receive the response. If the attempt still cannot be confirmed, wait for the webhook or reconcile through support tooling before issuing a new invite.

6d. Show the applicant the current state

The top-level status has the same meaning in both paths:

statusUI action
succeededShow the success screen – the applicant is done.
failedShow failure messaging (see failure reasons).
pendingShow an "Application is being reviewed" screen.

In the direct browser flow, collect documents before submission if your configuration needs them. A validation invite stops accepting document uploads once you create its validation submission.

Next, see Shared reference for status meanings, document rules, webhook events, and failure reasons.

7. Shared reference

Submission statuses

The status field will be one of:

StatusMeaning
succeededAll checks passed. The applicant qualifies.
failedOne or more checks failed. The applicant does not qualify.
pendingChecks are still running, or the applicant needs to provide more information.

When status === 'pending', the nested agentVerification.status tells you where the applicant check stands. It's useful context, but don't use it to decide whether to show a document-upload screen. During your implementation, Goodstack will consult with you on your configuration and let you know whether your checks require documents.

agentVerification.statusMeaningUI action
approvedAgent check passed; other checks still runningShow "in review" screen
pendingAgent check is still runningShow "in review" screen
pending_reviewGoodstack has queued the submission for manual reviewShow "in review" screen
pending_user_verificationGoodstack is reaching out to the applicant directlyShow "in review" screen
rejectedAgent verification failedShow failure messaging (see failure reasons)

Document requirements

Send documents to the Create Validation Submission Document endpoint as a multipart/form-data request.

IntegrationAuthLinked byWhen to upload
Server-sideSecret key (backend)validationSubmissionIdAfter the submission exists, when your configuration calls for documentation
Direct browserPublishable key (browser)validationInviteIdBefore you create the submission

Both modes accept jpg, png, and pdf files up to 5 MB.

Goodstack validates file content, not the label

Goodstack identifies the file type by reading the file's actual content – not its Content-Type header or file extension. The API will reject any file that is not a real JPEG, PNG, or PDF inside, even one labelled as such:

{
"error": {
"code": "upload/filetype_not_supported",
"title": "Bad request",
"message": "Filetype not supported. Supported types include: jpg,jpeg,png,pdf"
}
}

The most common cause is a file whose label doesn't match its contents – for example iPhone photos, which are often HEIC files labelled image/jpeg. To avoid rejected uploads:

  1. Validate the file's real content on your side, not its label or extension.
  2. If you can't identify the file, or it is another format (like HEIC), block it before sending – or convert it to JPEG/PNG first.
  3. Keep files under 5 MB.
Coaching the applicant

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: a student ID, an enrolment letter, official school correspondence, or – for teachers – a payslip showing the school name or a letter on school letterhead.

Receive and process results

Verification checks resolve asynchronously, so we recommend using webhooks for result updates. Goodstack notifies your system when each submission resolves, which avoids polling and keeps your integration responsive.

info

To receive results, you will need the Webhook Subscriptions API.

Use webhooks for state changes

Use webhooks for state changes in both sandbox and production. Use the Retrieve Validation Submission API for debugging, support tooling, reconciliation, or recovering from a missed webhook delivery – don't poll it as your primary path.

GET https://api.goodstack.io/v1/validation-submissions/validationsubmission_xxx
Authorization: <secret-key>

It returns the same submission shape as the create call, reflecting the latest state:

{
"data": {
"id": "validationsubmission_xxx",
"status": "succeeded",
"organisationId": "organisation_xxx",
"agentVerification": { "id": "agentverification_xxx", "status": "approved", "rejectionReasonCode": null },
"validationRequestId": "validationrequest_xxx",
"metadata": {}
},
"object": "ValidationSubmission"
}

Before subscribing, create server-side event handlers that trigger any required actions (account upgrades, follow-up emails, etc.). When your handlers are ready, register them via the Create Webhook Subscription endpoint.

info

Process webhooks quickly and return a 200 response as soon as you have verified and stored the event. If your handler is slow, Goodstack times out and retries. Use a queue if your processing is non-trivial.

Verify that an incoming webhook came from Goodstack using the standard signature-verification pattern. The Verifying webhooks section of the Webhooks concept page has a complete code example and details on the Goodstack-Signature header.

Events to subscribe to

The events for Education verification are:

  • validation_submission.created – fired as soon as you successfully POST to the create endpoint
  • validation_submission.succeeded – all checks in the configuration passed
  • validation_submission.failed – one or more checks failed
  • validation_submission.updated – fired when the outcome of an already-resolved submission changes (only if dynamic outcome changes are enabled on your account)

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": {}
}
}
}

In the direct browser flow, validationInviteId is populated with the invite the submission was created from.

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.

Handle 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 CodeWhat it meansWhat to surface to the applicant
fake_email_usedWe could not verify the applicant's email as belonging to the institution"We could not verify your school email. Please reapply using a valid school-issued address."
validation_request_failedWe 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_expiredThe applicant did not respond to a verification step in time"Your application has expired. Please reapply."
otherA 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.

Next steps