Skip to main content

Handle verification results

When you verify an organisation or applicant with a validation submission, the submission's status tells you whether to grant the benefit. On some programs, that status can change after the first result, in either direction.

This page explains when a result changes, which webhooks you receive, and how to build a handler that grants and removes benefits correctly. It applies to cause verification and education verification.

Read Webhooks first. It covers subscriptions, signatures, retries, and the store-then-process pattern this page builds on.

When a result can change​

Goodstack can change a submission's result after it's decided:

  • A succeeded submission can change to failed. For example, a later fraud review finds that the application was fraudulent.
  • A failed submission can change to succeeded. For example, a review confirms the applicant is eligible, or the applicant provides new information.

What to expect:

  • A changed result usually arrives 24 hours or more after the first one, and can take up to a week.
  • There's no cut-off, so don't treat any result as final.
  • Goodstack turns this on for each program. On programs without it, succeeded and failed don't change.
  • The handler on this page works either way, so you can build it before it's turned on for your program.

What's changing for existing programs​

Goodstack is turning on changing results for existing programs. Once it's on for yours:

  • a succeeded or failed submission can change status
  • Goodstack sends validation_submission.updated for every status change
  • a change to an agent verification result can change the submission's status

Before it's turned on for your program:

  1. Subscribe to validation_submission.updated as well as created, succeeded, and failed. See Update an existing subscription.
  2. Update your handler to work from the latest status you retrieve, not the event name. See Build your handler.
  3. Agree what happens to a benefit you've already granted if the submission changes to failed. See Decide what happens when a result changes. If you have questions, or want to know when this will happen for your program, contact your Goodstack account team.

Statuses​

StatusMeaningWhat to do
pendingGoodstack is still checking, or checking again.Keep the benefit gated.
processedChecks are still in progress. This isn't a result.Don't grant the benefit, and don't treat it as a failure.
succeededAll the checks in your program pass.Grant the benefit.
failedAt least one check failed.Keep the benefit gated, or follow your policy if you've already granted it.

The changes out of succeeded and failed only happen on programs where results can change.

Write your handler so it works from the current status, whatever came before:

  • If the status is succeeded, the applicant is verified.
  • If the status is failed, the applicant isn't verified.
  • If the status is anything else, including one your code doesn't recognise, the verification is in progress. Don't grant the benefit, and don't treat it as a failure.

A failed API request isn't a failed result. A submission only gets a failed status after Goodstack has created it and checked it.

Events you receive​

Subscribe to all four validation_submission events.

WhenGoodstack sends
A submission is createdvalidation_submission.created
The status changes to succeededvalidation_submission.succeeded and validation_submission.updated
The status changes to failedvalidation_submission.failed and validation_submission.updated
The status changes to anything else, such as pendingvalidation_submission.updated

On programs where results don't change, Goodstack doesn't send validation_submission.updated. You receive created, then succeeded or failed.

How to handle them:

  • Handle all four events in one code path. They have the same eventData.
  • Retrieve the submission and act on its status, not the event name. Events can arrive out of order, and the status in eventData can already be newer than the event.
  • Make the update safe to repeat. A change to succeeded or failed sends two events with different event IDs, so checking the event ID won't stop you processing the same change twice. See Build your handler.
  • Don't skip validation_submission.updated. It's the only event sent for every status change. Without it, you won't find out when a submission goes back to pending.
  • To spot a changed result, compare the status you retrieve with the one you've stored. No field in the event tells you.

Agent verification changes​

If your program checks that the applicant is associated with the organisation, that check is an agent verification. Its result can change too. When it does:

  • Goodstack sends a new agent_verification.approved or agent_verification.rejected event, in the same shape as the first decision.
  • On programs where submission results can change, Goodstack then works out the submission's status again, and sends the submission events above if it changes.

Which events you need:

  • If you grant benefits from the submission, as most programs do, you only need the submission events.
  • If you track applicants separately from submissions, also subscribe to agent_verification.approved and agent_verification.rejected.

Eligibility changes​

If you use eligibility subscriptions directly:

  • results.eligibilityStatus can change between pass and fail, for example when an organisation's activity tags change.
  • Goodstack sends eligibility_subscription.updated whenever the results change.
  • The event doesn't include the previous value, so compare it with the one you've stored.

Event data​

eventData contains the submission. Retrieve the submission before you act on it:

  • Goodstack builds eventData after the status change, so it can already include later changes.
  • A retry sends the same eventData as the first attempt, so it can be hours old by the time it arrives.
FieldTypeDescription
idstringThe validation submission ID. Use it to retrieve the latest state.
statusstringThe status when the event data was built. See Statuses.
createdAtstringWhen the submission was created (ISO 8601).
processedAtstring | nullWhen the submission was processed (ISO 8601).
metadataobjectThe metadata you sent when you created the submission, or {}. Use it to find your own record.
partnerFieldsobjectThe values of any additional fields set up for your program.
organisationIdstring | nullThe Goodstack organisation. null until we've validated an organisation that was entered manually.
organisationNamestring | nullThe organisation's name. Can be missing when there's no organisationId.
validationSubmissionHostedConfigurationIdstringThe program configuration for the submission. configurationId has the same value.
validationInviteIdstring | nullThe validation invite used to create the submission, if there was one.
validationRequestIdstring | nullThe validation request for an organisation that was entered manually, if there was one.
validationRequestDocumentIdstring | nullThe supporting document attached to that validation request, if there was one.
agentVerificationIdstring | nullThe agent verification, if your program has one.
eligibilitySubscriptionIdstring | nullThe eligibility check, if your program has one.
monitoringSubscriptionIdstring | nullThe compliance check, if your program has one.
agentVerificationobject | nullA summary of the agent verification.
validationRequestobject | nullA summary of the validation request.
failureReasonsarrayOnly present when status is failed. See Failure reasons.

The applicant's name and email aren't included at the top level of eventData. There's no updatedAt and no previous status.

agentVerification has these fields:

FieldTypeDescription
idstringThe agent verification ID.
firstNamestringThe applicant's first name.
lastNamestringThe applicant's last name.
emailstringThe applicant's email. Left out if the applicant was identified another way.
identifiersobjectOther identifiers for the applicant. Left out if there aren't any.
statusstringpending, pending_user_verification, pending_review, approved, or rejected.
rejectionReasonCodestring | nullWhy the agent verification was rejected, if it was.
rejectionReasonstring | nullDeprecated. Use rejectionReasonCode.

validationRequest has these fields:

FieldTypeDescription
idstringThe validation request ID.
namestringThe organisation name that was submitted.
countryCodestringThe organisation's country (ISO 3166-1 alpha-3).
websitestring | nullThe organisation website that was submitted.
organisationTypesarray | nullThe organisation types the request was checked against.
acceptedAtstring | nullWhen the request was accepted (ISO 8601).
rejectedAtstring | nullWhen the request was rejected (ISO 8601).

The nested statuses show progress, but the submission's top-level status is the result. Don't decide whether to grant a benefit from agentVerification.status or the validation request timestamps.

Here's the eventData for a submission that succeeded:

{
"id": "validationsubmission_xxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"status": "succeeded",
"createdAt": "2026-08-07T14:52:10.000Z",
"processedAt": "2026-08-07T14:58:41.000Z",
"metadata": {
"applicationId": "application_12345"
},
"partnerFields": {},
"organisationId": "organisation_xxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"organisationName": "Example Charity",
"validationSubmissionHostedConfigurationId": "hostedconfiguration_xxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"configurationId": "hostedconfiguration_xxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"validationInviteId": null,
"validationRequestId": null,
"validationRequestDocumentId": null,
"agentVerificationId": "agentverification_xxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"eligibilitySubscriptionId": "eligibilitysubscription_xxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"monitoringSubscriptionId": "monitoringsubscription_xxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"agentVerification": {
"id": "agentverification_xxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"firstName": "Alex",
"lastName": "Example",
"email": "alex@example.org",
"status": "approved",
"rejectionReason": null,
"rejectionReasonCode": null
},
"validationRequest": null
}

Get the check results​

eventData has IDs and summaries, but not the full check results. Eligibility and compliance results are only included in failureReasons, so a succeeded event won't tell you which activity sub-tags were confirmed. To get them, retrieve the submission:

GET https://sandbox-api.goodstack.io/v1/validation-submissions/validationsubmission_xxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Authorization: <secret-key>
Accept: application/json

See Retrieve a Validation Submission for the response.

Redacted applicant details​

Your program can redact any of firstName, lastName, and email in agentVerification:

  • A redacted field is still present, but its value is the string [REDACTED].
  • A field that would otherwise be left out stays left out, such as email when the applicant was identified another way.
  • Don't validate email as an email address.
  • Use metadata or the submission ID to find your own record, not the applicant's details.

Failure reasons​

When status is failed, failureReasons in the webhook lists the checks that failed. Each item has a check and a reason. The fields in reason depend on the check.

checkFields in reasonMeaning
validation_requestrejectionReasonCodeWe couldn't validate the organisation that was entered manually.
agent_verificationrejectionReasonCode, and sometimes documentReuploadLimitReachedWe couldn't verify that the applicant is associated with the organisation.
eligibilitystatus, resultsThe organisation didn't meet the eligibility criteria, or we couldn't determine it.
complianceresultsThe organisation didn't pass the compliance policy for your program.
country_compliancestatus, resultsThe organisation didn't pass the country compliance check for your program.
{
"id": "validationsubmission_xxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"status": "failed",
"failureReasons": [
{
"check": "agent_verification",
"reason": {
"rejectionReasonCode": "invalid_documentation",
"documentReuploadLimitReached": true
}
},
{
"check": "eligibility",
"reason": {
"status": "live",
"results": {
"eligibilityStatus": "fail",
"confirmedActivitySubTags": [],
"rejectedActivitySubTags": []
}
}
}
]
}

A few things to know:

  • If the validation request failed, it's the only reason in the array. The other checks can't run until we've validated the organisation.
  • Otherwise, you can get more than one reason. Agent verification, eligibility, and compliance failures can appear together.
  • documentReuploadLimitReached is only included when it's true. It means the applicant has used all the document re-uploads your program allows.
  • The array can be empty, and we can add checks and codes. Use status to decide the result, and send anything you don't recognise to your general review path.
  • failureReasons is only in the webhook. Retrieve a Validation Submission doesn't return it. See Get failure details from the API.

Get failure details from the API​

The submission's status is the result. If you need to know why a submission failed and you don't have the webhook, find the details for each check here:

CheckWhere to find the details
Agent verificationagentVerification.rejectionReasonCode in the retrieved submission.
Validation requestrejectionReasonCode from Retrieve a Validation Request, using the submission's validationRequestId.
Eligibilityeligibility.status and eligibility.results in the retrieved submission.
Complianceresults from Retrieve a Monitoring Subscription, using the submission's monitoringSubscriptionId.
Country complianceNot available from the API. Keep the country_compliance item from the webhook's failureReasons.

If you keep failure reasons from webhooks:

  • documentReuploadLimitReached is also only in the webhook.
  • Each failure reason describes the submission when that event was built. Don't let an older failure reason override the status you retrieve.

Fraud failures​

If an application fails Goodstack's fraud and risk checks, the agent verification is rejected with the rejectionReasonCode suspicious_application, and the submission changes to failed. You can find it in either place:

A failureReasons item with check set to agent_verification:

"failureReasons": [
{
"check": "agent_verification",
"reason": {
"rejectionReasonCode": "suspicious_application"
}
}
]

Any other code means the agent verification failed for a different reason, such as invalid_documentation.

SituationWhat you see
The fraud checks need a person to review itAgent verification is pending_review, and the submission stays in progress. Wait.
A fraud review fails a submission that succeededThe submission changes from succeeded to failed, on programs where results can change. See Decide what happens when a result changes.

Don't show failure reasons, raw codes, or compliance results to applicants. Use them for support, analytics, and choosing the recovery path.

Build your handler​

Use the endpoint and worker from Build your handler on the Webhooks page: verify the webhook, store it with a unique event ID, and respond. For each submission event, the worker retrieves the latest submission and updates your system.

The steps below are written as pseudo-code, so you can use them in any language.

WORKER process the next unprocessed validation_submission event
reconcileSubmission(event.eventData.id)
if it fails:
stop, and leave the event unprocessed so the worker tries it again
mark the event processed

reconcileSubmission(submissionId)
submission = GET /v1/validation-submissions/{submissionId}
if the request fails:
fail, and don't change anything
// A failed request isn't a failed result.

newStatus =
"verified" if submission.status is "succeeded"
"not_verified" if submission.status is "failed"
"in_progress" for anything else, including pending, processed,
and statuses your code doesn't recognise

// Do the rest in one database transaction, keyed on submission.id,
// so repeated events give the same result.
storedStatus = the status you've stored for submission.id
if newStatus is the same as storedStatus:
stop
save newStatus and submission.organisationId
save the action for the change from storedStatus to newStatus

The action for each change:

Stored statusNew statusWhat to do
AnyThe sameNothing. It's a duplicate, or the second event for the same change.
in_progressverifiedGrant the benefit.
in_progressnot_verifiedKeep the benefit gated and follow your recovery path.
verifiednot_verifiedFollow your policy for removing a benefit you've already granted.
not_verifiedverifiedGrant the benefit. The earlier failure has been reversed.
verifiedin_progressFollow your policy for a verification that's being checked again.
not_verifiedin_progressKeep the benefit gated, and wait for the next result.

If you run more than one worker, or your scheduled check can run at the same time as your worker, take a lock on the submission ID around reconcileSubmission. See Run more than one worker.

Grant and remove benefits​

How you grant and remove benefits is up to you. The guidance below is what we recommend, not a requirement.

We recommend storing whether you've delivered the benefit separately from the verification status. That way, if delivery fails after a succeeded result, the stored status can stay verified while you retry the delivery.

What we recommend depends on what granting or removing the benefit changes:

  • If it only changes your own database, such as setting a flag on the account, we recommend making the change in the same transaction that saves the status.
  • If it calls another system, such as a billing provider, the call can fail after you've saved the new status. The next event has the same status, so your handler won't try again. A pattern like the one below helps make sure the change still happens.

One way to make changes in another system reliably:

  1. In the transaction that saves the status, also save a task for the grant or removal, with its own ID.
  2. Have a separate job carry out saved tasks, retrying each one until it succeeds, then marking it as done.
  3. If the other system accepts an idempotency key, use the task ID, so a retry can't grant or remove the benefit twice.
  4. Run the tasks for each application one at a time, in the order you saved them, and check before each one that the application's stored status still needs it. This stops a delayed grant from running after a removal.

Decide what happens when a result changes​

Goodstack tells you that a result has changed. You decide what that means for the applicant.

When a succeeded submission changes to failed, you might:

  • downgrade the account or remove the benefit
  • suspend the account while you review it
  • close the account, if the change was caused by fraud
  • tell the applicant, and how they can contact you

When a failed submission changes to succeeded, grant the benefit and tell the applicant they now qualify.

Before your program goes live, or before changing results are turned on for it:

  • Agree these policies. Don't remove access without one in place.
  • Check what you send applicants. Don't include failure reasons, raw codes, or compliance results.

Reconcile results​

Webhooks are the main way to find out about a result, but don't rely on them alone. If your endpoint is down for longer than the retry window, you'll miss events.

Run a scheduled job for submissions that don't have a result yet:

  • Pass each one to reconcileSubmission, the same function your webhook worker uses. Use the same lock, if you have one.
  • Stop checking a submission once it has a result, or when it reaches the age limit agreed for your program.

You don't need to check submissions you've already acted on. Goodstack sends validation_submission.updated if their result changes.