Nonprofit verification: API
Use the Goodstack API to build a nonprofit verification flow inside your product. Your interface collects the application, your backend submits it to Goodstack, webhook events prompt updates, and reconciliation reads confirm the current outcome.
This guide covers one server-side integration from organisation search through entitlement. If you want Goodstack to host the applicant interface, use the hosted verification flow.
Before you start
Before you build the full flow, confirm that your publishable key can call the sandbox List Available Countries API. You will also need:
- a publishable key for organisation-discovery requests;
- a secret key for submissions, documents, reads, and webhook subscriptions;
- the
configurationIdthat Goodstack assigned to your program.
Retrieve your keys from the Keys page in the Partner dashboard. If the page or sandbox credentials are not available to your account, ask your Goodstack implementation contact to enable access.
Pass either API key as the raw value of the Authorization header. Do not add Bearer. Keep your
secret key on your backend and never expose it in browser code, mobile apps, logs, or source
control.
Before you build the applicant flow, confirm these details with your Goodstack implementation contact:
- which organisation types and countries your program accepts;
- whether applicants can enter an organisation that is not in search results;
- which applicant fields and supporting documents your checks require;
- whether an outcome can change after the first decision;
- the failure and reapplication experience you want applicants to see.
These choices affect request validation and client behavior. The configuration endpoints return basic configuration data, but do not expose every check or document requirement.
Use https://sandbox-api.goodstack.io while you build. The examples below use
<publishable-key> and <secret-key> placeholders for your sandbox credentials.
API map
These are the Goodstack requests used by the core flow and its manual-entry branch:
| Purpose | Request | Credential | Runs from |
|---|---|---|---|
| Populate the country selector | GET /v1/countries | Publishable key in a browser; secret key on your backend | Browser or your backend |
| Find a registered nonprofit | GET /v1/organisations | Publishable key in a browser; secret key on your backend | Browser or your backend |
| List registries for manual entry | GET /v1/registries | Publishable key in a browser; secret key on your backend | Browser or your backend |
| Create a verification | POST /v1/validation-submissions | Secret key | Your backend |
| Upload one supporting file when expected | POST /v1/validation-submission-documents | Secret key | Your backend |
| Register result delivery | POST /v1/webhook-subscriptions | Secret key | Your backend |
| Reconcile the current outcome | GET /v1/validation-submissions/{submissionId} | Secret key | Your backend |
The three discovery requests can run directly from the browser because they accept a publishable key. You can instead proxy them through your backend—for example, to centralise caching, filtering, or rate limiting. In that design, the browser calls your backend and only your backend calls Goodstack. A backend proxy can use either key, but must never return or forward a secret key to the browser.
Publishable-key requests are rate limited per source IP, while secret-key requests are rate limited per partner. When using a backend proxy, publishable-key traffic shares the proxy's outbound-IP allowance. Confirm current limits with Goodstack when planning higher-volume integrations.
How the flow works
1. Find the organisation
Ask first for the country where the organisation is registered. Goodstack uses three-letter ISO
country codes such as GBR, FRA, and USA. You can populate the country selector with the List
Available Countries API.
Search by country and organisation name with the Search Organisations API. This read can run from your front end with a publishable key:
GET https://sandbox-api.goodstack.io/v1/organisations?countryCode=GBR&type[]=nonprofit&query=community%20foundation
Authorization: <publishable-key>
Accept: application/json
You can also search with the exact registryId parameter. Present enough context for the applicant
to distinguish similarly named organisations.
A result is shaped like this:
{
"data": [
{
"id": "organisation_xxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"name": "Example Community Foundation",
"displayName": "Example Community Foundation",
"description": "A community foundation supporting local charities and projects.",
"countryCode": "GBR",
"types": ["nonprofit"],
"logo": "https://assets.example.org/example-community-foundation.png",
"registry": "registry_xxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"registryId": "1234567",
"registryDetails": {
"id": "registry_xxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"name": "Charity Commission",
"englishName": "Charity Commission",
"code": "CHC"
},
"website": "https://foundation.example.org",
"address": "10 Example Street, London, SW1A 1AA",
"addressLine1": "10 Example Street",
"addressLine2": null,
"city": "London",
"state": "England",
"postcode": "SW1A 1AA"
}
],
"totalResults": 1,
"pageNumber": 1,
"pageSize": 25,
"exhaustiveTotalResults": true,
"object": "organisation"
}
Some organisation fields can be null or absent. Use the registry number, location, and website to
help applicants distinguish similarly named results; use the selected organisation's id as the
stable API identifier.
When the applicant selects a result, store its id. Send that value as organisationId when you
create the submission.
When the organisation is not found
Most nonprofit programs allow applicants to enter an organisation that is not in the search results. When your program configuration allows it, offer a manual entry path and replace the search results with a form that collects the organisation's details. Goodstack uses this information to perform a Validation Request as part of the validation submission and assess the organisation's identity and nonprofit status. If the path is not enabled for your program, keep the applicant in your approved search or support flow instead of sending a manual-entry payload.
Give the applicant a way to return to search, and explain that continuing starts an organisation review that normally requires official supporting evidence.
An illustrative front-end structure could look like this; adapt the wording to your approved applicant experience:
Try another name or registry number, or submit the organisation for review.
What happens next In most cases, you will provide official evidence of the organisation's nonprofit status for Goodstack to review.
Carry the country selected for search into this form, while allowing the applicant to correct it. Use the List Registries API to populate the registry selector for that country:
GET https://sandbox-api.goodstack.io/v1/registries?countryCode=GBR
Authorization: <publishable-key>
Accept: application/json
Submit the selected registry's name as registryName, not its Goodstack id. Submit the
organisation's own record number in that registry as registryId. If the correct registry is not
listed, registryName also accepts the registry's official name as free text.
For example:
| Organisation jurisdiction | registryName example | registryId example |
|---|---|---|
| England and Wales | Charity Commission | Charity number, such as 1234567 |
| United States | Internal Revenue Service | EIN, such as 12-3456789 |
Use the exact name returned by the Registries API when one is available; the examples show how the
registry name and the organisation's identifier map to separate submission fields.
For the nonprofit-only configuration covered by this guide, collect these organisation details:
| Field | Front-end guidance |
|---|---|
organisationName | Ask for the organisation's public name. |
registryName | Offer the official registries returned for the selected country, plus free text. |
registryId | Label this for the selected registry, such as “charity number” or “EIN.” |
website | Ask for the organisation's official website. |
countryCode | Carry forward the selected three-letter ISO country code. |
These requirements are scoped to the nonprofit-only manual path.
When the applicant continues, collect their details and preferred language, then send everything to your backend using the manual-entry submission payload. Store the returned validation-submission ID before uploading the expected organisation evidence in step 4. Keep the benefit gated while Goodstack reviews the organisation and the other configured checks.
This guide's examples target a nonprofit-only configuration. If your program also accepts
social_impact organisations, confirm the search filters and manual-entry requirements for that
configuration before reusing these examples.
2. Create the validation submission
Your backend creates a submission with its secret key. Always pass the configurationId assigned to
the program; do not rely on an account default. You can list the IDs available to your account with
the Retrieve Partner Configurations API.
The examples below assume that applicant/agent verification is enabled, so they include
firstName, lastName, and email. Goodstack will confirm the fields required by your actual
configuration.
- Found in search
- Manual entry
Use this payload when the applicant selected a search result:
POST https://sandbox-api.goodstack.io/v1/validation-submissions
Authorization: <secret-key>
Idempotency-Key: cause_attempt_<your-id>_submission
Content-Type: application/json
{
"configurationId": "hostedconfiguration_xxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"organisationId": "organisation_xxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"firstName": "Alex",
"lastName": "Doe",
"email": "alex.doe@example.org",
"language": "en-GB",
"metadata": {
"applicationId": "application_12345"
}
}
Use this payload when the applicant entered the organisation's details manually:
POST https://sandbox-api.goodstack.io/v1/validation-submissions
Authorization: <secret-key>
Idempotency-Key: cause_attempt_<your-id>_submission
Content-Type: application/json
{
"configurationId": "hostedconfiguration_xxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"organisationName": "Example Community Foundation",
"registryName": "Charity Commission",
"registryId": "1234567",
"website": "https://foundation.example.org",
"countryCode": "GBR",
"firstName": "Alex",
"lastName": "Doe",
"email": "alex.doe@example.org",
"language": "en-GB",
"metadata": {
"applicationId": "application_12345"
}
}
metadata lets you attach up to 20 of your own string values to the submission. Keys can contain up
to 40 characters and values up to 250 characters. Include a stable application or account ID so
webhook events can be reconciled with your system without using applicant details as the lookup key.
Create one verification-attempt record in your system before calling Goodstack. Use one stable
Idempotency-Key for that logical create request. If the request times out, retry with the same key
and the identical payload.
Use a different key for every other logical write, including each document upload. Never reuse a key with a changed payload or across two endpoints. Do not build around a fixed key-retention window.
Store the response
Goodstack returns 200 OK with the current submission. Persist data.id before updating the
applicant experience. The other fields and nested check objects depend on the configured checks:
{
"data": {
"id": "validationsubmission_xxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"status": "pending",
"organisationId": "organisation_xxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"validationRequestId": null,
"agentVerificationId": "agentverification_xxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"eligibilitySubscriptionId": "eligibilitysubscription_xxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"monitoringSubscriptionId": "monitoringsubscription_xxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"createdAt": "2026-08-07T14:30:00.000Z",
"metadata": {
"applicationId": "application_12345"
}
},
"object": "validation_submission"
}
Store:
data.idas the canonical validation-submission ID;data.organisationIdwhen present;- each non-null component ID that your configuration returns;
- your own application ID and the idempotency key used for the create request.
A manual submission starts with organisationId: null and has a validationRequestId.
If Goodstack validates the organisation, later reads and webhooks include its organisationId.
Component IDs can be null when their checks are not configured, so your integration must not require
every ID in the example.
3. Show the applicant the current state
Drive the main applicant experience from the top-level submission status:
| Status | Client action |
|---|---|
pending | Keep the benefit gated. Show an in-review state or the configured next step. |
succeeded | If your approved program policy maps success to access, apply the benefit. Store any newly assigned organisationId. |
failed | Keep the benefit gated and show your approved failure or recovery experience. |
Do not assume the create response will be pending; handle all three statuses. Checks run
asynchronously and can take from a few seconds to as long as 72 hours. When the response is
pending, release the request and use result delivery and reconciliation rather than keeping the
applicant's browser request open.
Nested check statuses can explain progress, but they do not replace the top-level outcome.
In particular, do not infer that the applicant needs association evidence from
agentVerification.status. Follow the manual-entry evidence guidance in step 4, and use the
requirements Goodstack confirmed for any additional document steps.
Some configurations can recalculate an outcome after the initial decision. If Goodstack confirms
that dynamic outcomes are enabled for your program, process validation_submission.updated and
apply your agreed entitlement or review policy to the latest retrieved status.
4. Upload supporting documents
Plan to collect organisation evidence when an applicant uses manual entry. Almost all manual-entry configurations require it because the organisation was not selected from Goodstack search results. Only omit this step when Goodstack explicitly confirms that your configuration is one of the rare exceptions.
For an organisation selected from search results, request a document only when your configuration requires one. Goodstack may also require separate evidence of the applicant's association with the organisation.
Recommend evidence for a manually entered nonprofit
For the typical manual-entry path, ask the applicant for the clearest current official evidence of the organisation's nonprofit or charitable status. Recommend one or more of these documents, starting with the strongest evidence available in the organisation's jurisdiction:
- a registration, recognition, or good-standing certificate issued by a charity, nonprofit, association, foundation, or other competent regulator;
- a current official registry extract, status confirmation, registration decision, or renewal notice;
- an official-gazette notice or government registration receipt that establishes the organisation's legal existence;
- a tax-authority determination or recognition letter that explicitly confirms charitable, nonprofit, or tax-exempt status;
- a certificate of incorporation, formation, or registration that identifies the entity's nonprofit legal form.
If no single official document establishes both legal identity and nonprofit status, the applicant can also provide a constitution, articles of association, bylaws, trust deed, foundation charter, or similar governing document alongside the strongest official evidence available.
Prefer documents that clearly show the legal name, registration or charity number, issuing authority, jurisdiction, status, and any issue, renewal, or expiry date. These details should match the manual-entry payload. Do not use a donation receipt, bank statement, website screenshot, or marketing material as the primary proof of nonprofit status.
Upload nonprofit-status evidence with documentType=validation_request. Use
documentType=agent_verification only when the document instead proves the applicant's association
with the organisation.
Your backend sends the file to the Create Validation Submission Document API with its secret key:
curl --request POST \
--url 'https://sandbox-api.goodstack.io/v1/validation-submission-documents' \
--header 'Authorization: <secret-key>' \
--header 'Idempotency-Key: cause_attempt_<your-id>_document_1' \
--form 'validationSubmissionId=validationsubmission_xxxxxxxxxxxxxxxxxxxxxxxxxxxxx' \
--form 'documentType=validation_request' \
--form 'file=@./registration-proof.pdf'
Let your HTTP client set the multipart Content-Type, including its boundary. Upload one file per
request.
Choose documentType by what the evidence supports:
documentType | Use it for |
|---|---|
validation_request | Evidence about the organisation, such as registration documentation |
agent_verification | Evidence about the applicant's association with the organisation |
Send the value explicitly rather than relying on the default.
A successful upload returns:
{
"data": {
"id": "validationsubmissiondocument_xxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"validationSubmissionId": "validationsubmission_xxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"createdAt": "2026-08-07T14:35:00.000Z",
"url": "https://assets.example.org/document.pdf",
"type": "validation_request"
},
"object": "validation_submission_document"
}
The endpoint accepts JPEG, PNG, and PDF content up to 5 MB. Goodstack detects the file type from the
file's bytes, not its extension or Content-Type label. A HEIC image renamed to .jpg, for example,
is still rejected. Validate or convert files before upload, and do not log document contents or
presigned document URLs.
After upload, store data.id with the verification attempt, keep the benefit gated, and show the
applicant the in-review state. The upload response is not an outcome; wait for a webhook or the next
scheduled reconciliation read.
5. Configure result delivery
Complete result delivery before accepting applications. Keeping this setup next to event processing
makes the full asynchronous result path easier to implement and test. Use the Create Webhook
Subscription API from your backend with your secret key.
The request uses the plural events property and requires an HTTPS URL:
POST https://sandbox-api.goodstack.io/v1/webhook-subscriptions
Authorization: <secret-key>
Content-Type: application/json
{
"events": [
"validation_submission.created",
"validation_submission.succeeded",
"validation_submission.failed",
"validation_submission.updated"
],
"url": "https://example.org/webhooks/goodstack"
}
The create response contains the secret used to verify this subscription's deliveries:
{
"data": {
"id": "webhooksubscription_xxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"events": [
"validation_submission.created",
"validation_submission.succeeded",
"validation_submission.failed",
"validation_submission.updated"
],
"url": "https://example.org/webhooks/goodstack",
"secret": "<webhook-subscription-secret>",
"createdAt": "2026-08-07T14:00:00.000Z",
"updatedAt": null,
"deletedAt": null
},
"object": "webhook_subscription"
}
Store data.id and data.secret in your backend's secret store as soon as you receive them. The
subscription secret is separate from both API keys; do not expose it to applicants, commit it, or
write it to logs. Subscribe to all four events so the same handler works if dynamic outcomes are
enabled later.
6. Process and reconcile results
The endpoint configured in step 5 receives these Validation Submission events:
validation_submission.created;validation_submission.succeeded;validation_submission.failed;validation_submission.updated.
The updated event is used by configurations whose outcomes can change.
An event uses this envelope:
{
"object": "event",
"data": {
"id": "event_xxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"createdAt": "2026-08-07T15:00:00.000Z",
"eventType": "validation_submission.succeeded",
"eventData": {
"id": "validationsubmission_xxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"status": "succeeded",
"organisationId": "organisation_xxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"agentVerificationId": "agentverification_xxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"eligibilitySubscriptionId": "eligibilitysubscription_xxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"monitoringSubscriptionId": "monitoringsubscription_xxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"validationSubmissionHostedConfigurationId": "hostedconfiguration_xxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"metadata": {
"applicationId": "application_12345"
}
}
}
}
Treat delivery as an at-least-once signal:
- Verify
Goodstack-Signatureagainst the raw request body with the webhook subscription's secret, not an API key. Compute the hex-encoded HMAC-SHA256 over the unmodified body and compare the received and expected signatures in constant time. - Deduplicate delivery on the event
data.id. - Persist the event and enqueue any slow work.
- Return a
2xxresponse after the event is safely stored. - Retrieve the current submission before applying a state transition that must be authoritative.
Make the state or entitlement update idempotent by submission ID and the latest retrieved state, not only by event ID. Distinct events can describe the same effective outcome, and retries or out-of-order deliveries must not grant a benefit twice or overwrite a newer state.
Retrieve the latest submission
Use webhooks as the prompt for state changes. Use the Retrieve Validation Submission API as the authoritative read for reconciliation, support tooling, or recovery after an uncertain delivery:
GET https://sandbox-api.goodstack.io/v1/validation-submissions/validationsubmission_xxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Authorization: <secret-key>
Accept: application/json
The response contains the latest submission and configured check results:
{
"data": {
"id": "validationsubmission_xxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"status": "succeeded",
"organisationId": "organisation_xxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"agentVerification": {
"id": "agentverification_xxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"status": "approved",
"rejectionReasonCode": null
},
"eligibility": {
"status": "live",
"results": {
"eligibilityStatus": "pass",
"confirmedActivitySubTags": [],
"rejectedActivitySubTags": []
}
},
"metadata": {
"applicationId": "application_12345"
}
},
"object": "validation_submission"
}
Nested check objects and result keys are configuration-dependent. Treat the read's top-level
status—not a partial webhook snapshot or a nested check status—as the current verification
outcome.
Do not assume that every non-terminal transition produces a webhook. Run a bounded reconciliation job that periodically retrieves submissions for which your system has not recorded a terminal outcome, and stops or escalates them according to the cadence and age limits agreed during onboarding. If dynamic outcomes are enabled, also follow the agreed resynchronization policy after a terminal outcome. Apply the same idempotent state-transition logic to webhook-triggered and scheduled reads.
7. Deliver the benefit and communicate the outcome
After retrieving the latest submission, use its top-level status to complete the applicant journey. Keep the verification outcome separate from your own benefit-fulfilment state: Goodstack determines whether the verification succeeded, while your system determines whether and when the benefit has been delivered.
Complete the happy path
When status is succeeded and your program policy maps success to the benefit:
- Persist the successful outcome and the latest
organisationIdagainst your application. - Create or update one fulfilment record for the verification attempt.
- Grant the benefit, account access, discount, entitlement, or other program outcome exactly once.
- Record whether fulfilment is pending, complete, or needs operational attention.
- Communicate the result and the next step to the applicant.
Key fulfilment on your application or validation-submission ID so processing the same webhook again cannot grant the benefit twice. Acknowledge the webhook after storing the event; benefit fulfilment can run asynchronously through your normal job or queue infrastructure.
Tell the applicant what they need to know about both verification and fulfilment:
| Fulfilment state | Applicant communication |
|---|---|
| Complete | Confirm verification, name the benefit now available, and explain how to access or use it. |
| In progress | Confirm verification, explain that benefit delivery is underway, and give the next update or expected timing if known. |
| Needs operational help | Keep the verification marked successful, explain that benefit delivery is delayed, and provide the appropriate support path. |
Use your own applicant-facing application reference where one is helpful. Do not include API keys, webhook details, raw check or screening results, uploaded-document links, or internal failure data in the confirmation. If the benefit has limits, an expiry, or another activation step, include that information so the applicant knows what to do next.
If dynamic outcomes are enabled, agree how a later status change affects an already delivered benefit and how you will communicate that change. Do not silently remove access without the review and communication policy agreed for the program.
When the submission status is failed
A validation_submission.failed event is a verification outcome, not an API error. Persist the
outcome against the verification attempt, keep the benefit gated, and show the failure or recovery
experience agreed for your program.
A failed event can include a failureReasons array with structured context about the checks that
produced the outcome. For a typical manual-entry failure, it can look like this:
{
"object": "event",
"data": {
"id": "event_xxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"createdAt": "2026-08-07T16:00:00.000Z",
"eventType": "validation_submission.failed",
"eventData": {
"id": "validationsubmission_xxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"status": "failed",
"organisationId": null,
"validationRequestId": "validationrequest_xxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"metadata": {
"applicationId": "application_12345"
},
"failureReasons": [
{
"check": "validation_request",
"reason": {
"rejectionReasonCode": "incorrect_documentation"
}
}
]
}
}
}
Use each check value to choose an internal handling path:
check | What it tells you |
|---|---|
validation_request | Goodstack could not validate the manually entered organisation. |
agent_verification | Goodstack could not verify the applicant's association with the organisation. |
eligibility | The organisation did not meet the configured eligibility criteria, or its eligibility could not be determined. |
compliance | The organisation did not pass the configured compliance policy. |
| Other or absent | Keep the benefit gated and route the attempt through your generic support or review path. |
The shape of reason depends on the check. Validation Request and agent-verification failures can
include a rejectionReasonCode; eligibility and compliance failures contain their relevant result
data. Treat check, reason, and any nested values as extensible. Use them as machine-readable
context for support, analytics, and recovery routing, not as applicant-facing copy. In particular,
do not expose raw compliance screening results.
A Validation Request failure may be the only item because downstream checks cannot proceed until the organisation is validated. The array therefore explains the outcome; it is not necessarily a list of every check that would have failed.
Decide the recovery path
Agree during onboarding which outcomes allow the applicant to correct information, replace a document, contact support, or start another attempt. Do not automatically create a new submission whenever a failed webhook arrives.
If your program permits reapplication, treat it as a new logical verification attempt with a new create-submission idempotency key, and retain the earlier submission ID for audit and support. If dynamic outcomes are enabled, a failed submission can change later; apply the agreed dynamic-outcome policy before starting a competing attempt.
Troubleshoot API request failures
An unsuccessful API request is not a verification outcome. A request can fail before Goodstack
accepts or completes it; a submission reaches failed only after Goodstack has created the
verification and evaluated its configured checks. Do not show an applicant a verification-failure
message solely because an API request failed.
| Situation | Handling |
|---|---|
400 validation error | Inspect the error response and correct the request. A changed write is a new logical request and needs a new idempotency key. |
401 or 403 | Treat this as an integration configuration problem. Check the key, environment, and account scope before retrying. |
404 | Check the endpoint, resource ID, configuration ID, and whether sandbox and production values were mixed. |
429 or a 5xx response | Retry with backoff and honor Retry-After when present. For a write, keep the request body and idempotency key unchanged. |
| Timeout or lost response | The result is uncertain. Retry the identical request with the same idempotency key, then reconcile before creating another verification attempt. |
Keep the applicant's benefit gated while the request is unresolved. Record enough context to diagnose the request, but do not log API keys, uploaded documents, or other sensitive payload data.
Go live
Before changing to https://api.goodstack.io:
- switch the base URL and API keys together so sandbox and production credentials cannot mix;
- confirm the production
configurationIdand document requirements; - verify and deduplicate webhooks in a staging-like environment;
- test each public status and every configured document path;
- test benefit fulfilment retries without granting the benefit twice;
- approve applicant messages for success, delayed fulfilment, failure, and reapplication;
- confirm how your product grants, withholds, or revisits the benefit;
- ensure logs and analytics contain IDs, not secret keys or document contents.
Next steps
- Compare this flow with hosted nonprofit verification.
- Review the Create Validation Submission, Retrieve Validation Submission, and document upload references.
- Apply the webhook verification requirements in step 6 before accepting live events.