AI Assistant & integrations

API & webhooks (developers)

Order checks, receive check and adverse-action events programmatically.

Settings → API — create keys for the REST API and webhooks. · click to enlarge

VolunteerBadge has a simple REST API for managing volunteers, sending applications, ordering background checks, and receiving real-time events. If you'd rather not write code, see Import volunteers for the spreadsheet importer, or Connect to Claude to drive everything in plain English.

Base URL

All endpoints live under https://www.volunteerbadge.com/api/v1. Requests and responses are JSON.

Authentication

Create an API key in Settings → API(you'll accept the CRA End-User Agreement the first time). Keys look like vb_live_… — treat them like a password and never expose one in client-side code. Send it as a bearer token on every request:

Authorization: Bearer vb_live_xxxxxxxxxxxxxxxxxxxx

Identity verification required first (live keys)

Before you can create a live API key or make a live API call, an owner or admin on the account must complete a free, one-time identity verification in Settings → Account (a quick government-ID + selfie check, about two minutes). Until then, live requests return 403 identity_verification_required. Sandbox test keys are exempt — see below — so you can start building right away.

Fair use & abuse monitoring

API and MCP traffic is rate-limited (100 requests per minute per key) and monitored for abuse — key guessing, volume spikes, and sustained error storms are flagged automatically. Abusive access may be suspended while we investigate; suspended requests return 403 api_access_suspended. Questions? support@screenforgelabs.com.

Sandbox / test mode

Build and test your whole integration — including webhooks — before going live, using a sandbox key. In Settings → API, click “Create sandbox test key”. Sandbox keys start with vb_test_ (live keys start with vb_live_) and can be created with no agreement and no identity verification.

A test key hits the same endpoints with the same request shape. In sandbox:

  • No credit is charged and no real check runs — nothing reaches the screening vendor.
  • You get an instant simulated response, and your registered webhooks fire for real (check.complete / check.error), so you can validate your handler end-to-end.
  • Nothing is persisted — POST /api/v1/volunteers, PATCH /api/v1/volunteers/{id}, POST /api/v1/volunteers/{id}/rescreen, and POST /api/v1/applications return simulated IDs without writing a roster row or sending an invite.

The outcome is deterministic from the subject you send (like a test card number):

Send thisSimulated result
lastName: "Records"Records found — result "consider" (fires check.complete)
SSN ending 0000Same — records found
lastName: "Error"The check fails (fires check.error)
anything elseClear result (fires check.complete)

Sandbox check IDs are prefixed chk_test_, and every sandbox response includes "mode": "test". When you're ready for production, generate a vb_live_ key (that one requires the CRA agreement + identity verification, since it furnishes real consumer reports) and run the exact same code.

Endpoints

Method & pathWhat it does
GET /api/v1/volunteersList volunteers (paginated, filter by status). Includes rescreenDueAt.
GET /api/v1/volunteers/{id}Get one volunteer, including rescreenDueAt and their checks.
POST /api/v1/volunteersCreate a volunteer (idempotent on email).
PATCH /api/v1/volunteers/{id}Archive or restore (status "archived" or "approved"). Archived volunteers are excluded from auto-rescreen.
POST /api/v1/volunteers/{id}/rescreenSend the short FCRA re-screen consent link (not a full application).
GET /api/v1/templatesList your application templates — where the templateId comes from.
POST /api/v1/applicationsSend a volunteer application by email, SMS, or shareable link.
POST /api/v1/checksOrder a background check for a subject (name + DOB + SSN + address).
POST /api/v1/checks/instantCertified no-SSN instant check (name + DOB + state) — requires authorizationOnFile + permissiblePurpose.
GET /api/v1/checks/{id}Get a check’s status and result.
GET /api/v1/creditsGet your remaining check-credit balance.

Create a volunteer

Push a person into VolunteerBadge — e.g. migrating a roster or syncing from another system. Posting the same email twice returns the existing record instead of duplicating it.

curl -X POST https://www.volunteerbadge.com/api/v1/volunteers \
  -H "Authorization: Bearer vb_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "first_name": "Jamie",
    "last_name": "Rivera",
    "email": "jamie@example.com",
    "phone": "443-790-8081",
    "dob": "1981-05-29",
    "state": "FL",
    "last_check_date": "2025-06-01",
    "fcra_authorization_on_file": true,
    "status": "approved"
  }'

# → { "volunteerId": "f883...", "created": true }

fcra_authorization_on_file is required

Set it to trueonly for people whose signed FCRA authorization you actually hold — it's what allows checks (and auto-rescreen) to run for them.

List volunteers

curl "https://www.volunteerbadge.com/api/v1/volunteers?status=approved&page=1&limit=25" \
  -H "Authorization: Bearer vb_live_..."

# → { "volunteers": [ { "id": "...", "first_name": "...", "status": "approved",
#                       "rescreenDueAt": "2026-06-01T00:00:00.000Z", ... } ],
#     "pagination": { "page": 1, "limit": 25, "total": 42, "totalPages": 2 } }

Get one volunteer

curl https://www.volunteerbadge.com/api/v1/volunteers/f883... \
  -H "Authorization: Bearer vb_live_..."

# → { "id": "...", "firstName": "Jamie", "status": "approved",
#     "rescreenDueAt": "2026-06-01T00:00:00.000Z", "checks": [ ... ] }

Archive or restore a volunteer

Archiving removes someone from auto re-screen without deleting records (FCRA retention). Restore with { "status": "approved" } — that restore still requires a completed background check on file, same as the dashboard.

curl -X PATCH https://www.volunteerbadge.com/api/v1/volunteers/f883... \
  -H "Authorization: Bearer vb_live_..." \
  -H "Content-Type: application/json" \
  -d '{ "status": "archived" }'

# → { "id": "f883...", "status": "archived" }

Archive is blocked (HTTP 409) while a decision is pending or FCRA adverse action is incomplete.

Send a re-screen consent link

This emails (default), texts, or returns a link for the short FCRA authorization form — not a full application. The volunteer confirms their address, signs, and the $5 check runs. Prior authorization is not reused. Archived volunteers must be restored first. SMS requires smsConsentAttested: true.

curl -X POST https://www.volunteerbadge.com/api/v1/volunteers/f883.../rescreen \
  -H "Authorization: Bearer vb_live_..." \
  -H "Content-Type: application/json" \
  -d '{ "deliveryMethod": "email" }'

# → { "inviteId": "...", "consentUrl": "https://www.volunteerbadge.com/rescreen/...",
#     "deliveryMethod": "email", "expiresAt": "..." }

List application templates

Every application you send is built from one of your templates, and POST /api/v1/applicationsneeds that template's id. This is how you look them up:

curl https://www.volunteerbadge.com/api/v1/templates \
  -H "Authorization: Bearer vb_live_..."

# → { "templates": [
#      { "id": "3f9a1c22-7b64-4e08-9d15-8a2c7e4b1f60",
#        "name": "Youth & Children", "sector": "youth",
#        "isDefault": true, "fieldCount": 14, "sendCount": 37,
#        "createdAt": "...", "updatedAt": "..." } ] }

Your defaulttemplate is returned first, so an integration that just wants “the normal one” can take templates[0].idwithout sorting. Only your own organization's templates are ever returned. The question definitions themselves aren't included — fieldCount is there to tell similar templates apart.

ID formats in sandbox

Sandbox responses return real UUIDs for applicationId and volunteerId, so strict RFC 4122 validation behaves the same in test and production. Sandbox responses are identified by "sandbox": true and "mode": "test", never by the shape of an id.

Two deliberate exceptions: a sandbox check id is prefixed chk_test_ (the simulated outcome is encoded in it, which is what lets GET /api/v1/checks/{id} return a result for a check that was never stored), and a sandbox apply link token is prefixed sbx_. Opening that link shows a labelled sandbox page explaining that the invite worked and nothing was emailed — no real application sits behind it.

Send a volunteer application (consent-first invite)

Emails or texts the applicant your application, so they complete the FCRA disclosure and sign the authorization themselves. This is the path to use when you don't already hold a signed authorization — and the one that supports applicants with no middle name, since they attest to that on the signed form.

curl -X POST https://www.volunteerbadge.com/api/v1/applications \
  -H "Authorization: Bearer vb_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "templateId": "3f9a1c22-7b64-4e08-9d15-8a2c7e4b1f60",
    "email": "jamie@example.com"
  }'

# → { "applicationId": "...", "applyUrl": "https://www.volunteerbadge.com/apply/<token>",
#     "deliveryMethod": "email", "expiresAt": "2026-09-01T00:00:00.000Z" }

templateId is the only required field. Get it from GET /api/v1/templates above — or, if you just want to paste one in by hand, open Recruit → Applications, click the template to edit it, and take the UUID from the end of the browser URL (/dashboard/applications/builder/<templateId>). A template that doesn't belong to your organization returns 404.

Send it byWhat to postYou get back
EmailInclude email.The invite is emailed; deliveryMethod: "email".
SMSInclude phone.The invite is texted; deliveryMethod: "sms".
Shareable linkPost neither (or deliveryMethod: "link").Nothing is sent — hand out the applyUrl yourself.

Waiving identity verification for one invite

Settings → Screening → “Require identity verification” is an organization-wide default, but a single invite can override it — the same way the per-invite payer choice overrides your org's payer default. Use it when you confirm someone's ID another way, e.g. an elderly volunteer whose ID you check in person at intake and who would struggle with the selfie step.

curl -X POST https://www.volunteerbadge.com/api/v1/applications \
  -H "Authorization: Bearer vb_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "templateId": "3f9a1c22-7b64-4e08-9d15-8a2c7e4b1f60",
    "email": "jamie@example.com",
    "requireIdVerification": false,
    "idVerificationReason": "ID checked in person at intake on 2026-08-18"
  }'

Omit requireIdVerification and the invite inherits your org setting. Set it to true to require verification for one person even when your org default is off. Setting it to false requires idVerificationReason (10 characters or more): waiving the step that ties a real person to the record being screened is worth a note, and we store it on the invite so anyone reviewing that volunteer later can see how their identity was confirmed.

You do not need a second organization or a second API key for this.

deliveryMethod is inferred from what you send

The channel is decided by which contact field is present, not by the deliveryMethod value: post an email and the invite is emailed even if you also pass deliveryMethod: "link". To get a link and send nothing, omit both email and phone. Posting neither a contact field nor deliveryMethod: "link" returns 400.

The returned applyUrlis the same link the applicant receives, so you can show it in your own UI or fall back to it if someone doesn't get the email.

Order a background check

Requires firstName, middleName, lastName, dob, ssn, address, city, state, zip. middleName is required by default— it confirms identity and reduces false matches on common names. This endpoint runs the same SSN trace/address-history lookup as an invited applicant's check. This consumes 1 credit.

curl -X POST https://www.volunteerbadge.com/api/v1/checks \
  -H "Authorization: Bearer vb_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "firstName": "Jamie", "middleName": "Alex", "lastName": "Rivera",
    "dob": "1981-05-29", "ssn": "123456789",
    "address": "12112 Blue Hill Trail", "city": "Lakewood Ranch",
    "state": "FL", "zip": "34211"
  }'

# → { "checkId": "...", "status": "processing", "result": null, "message": "..." }

Screening scope follows Settings → Screening

Omit includeMisdemeanors and includeTrafficViolations— the recommended default — and the check uses your org's Settings → Screening defaults, the same ones invited volunteer checks use. Pass either flag as true or false to override that one setting for this request only. The flags are independent: you can override one and leave the other to the org default.

# Optional — omit both fields to inherit org settings
    "includeMisdemeanors": false,
    "includeTrafficViolations": true

Subscribe to check.complete (search finished) and check.adjudicated (human review finished, for checks with potential records) and you never need to poll. GET /api/v1/checks/{id} is there whenever you want to read the current state directly.

Omitting middle name on the API

middleName is required on POST /api/v1/checks and POST /api/v1/checks/instant unless you explicitly pass "requireMiddleName": false in the JSON body. That flag is an intentional opt-out — omitting it does not waive the requirement. Do not send placeholders like "N/A" or "NMN"; use the opt-out when you genuinely don't have a middle name.

The dashboard Run background check drawer always requires a middle name (or initial). For someone with no middle name, use the consent-first invite (POST /api/v1/applications) so they can tick “I have no middle name” on the signed application.

Check a check's status

Use the checkId returned by POST /api/v1/checks or POST /api/v1/checks/instant exactly as given — no prefix, no encoding:

curl https://www.volunteerbadge.com/api/v1/checks/8f2b1c34-5d6e-4a70-9b81-2c3d4e5f6a7b \
  -H "Authorization: Bearer vb_live_..."

# → { "checkId": "...", "volunteerId": null, "status": "complete", "result": "clear",
#     "type": "direct", "creditsCharged": 1,
#     "offenderCount": 0, "errorMessage": null,
#     "completedAt": "...", "createdAt": "...",
#     "reportUrl": "...", "reportDownloadUrl": "..." }

type is direct for checks you order through /checks or /checks/instant, and fullfor a check produced by a volunteer's signed application. The response includes a legacy ssnTraceIncluded field that defaults to true in the database — do not branch on it. It does not reliably indicate whether an SSN trace actually ran: certified no-SSN checks (/checks/instant) have no trace even though the field may read true. SSN-bearing checks run a trace; check the report or dashboard SSN Trace card for the actual address-history result.

A check is only ever visible to the organization that ordered it — another org's checkId returns 404, not the record.

statusMeaning
processingStill running. Keep polling, or wait for the webhook.
completeFinished and clear — result is "clear".
pending_adjudicationPotential records found; a trained CRA reviewer is verifying them before release (typically within 3 business days). result stays null until that finishes.
adjudicatedThe terminal state for a records-found check. Review is finished: result is "clear" when the records were excluded as not this person / not reportable, or "consider"when records were released onto the report. Treat this as done — don't wait for complete, which a records check never reaches.
errorThe check could not be completed — see errorMessage. The credit is returned.
refundedThe check was refunded; no result will follow.
hits_foundTransitional, and rarely seen through the API — potential records were returned and are being routed for review.
pending_auto_releaseTransitional — a clear result queued for release.
pendingOrdered but not started yet.

reportDownloadUrl is a short-lived signed link, generated per request once the check is no longer processing — fetch it when you need it rather than storing it.

A records-found check sends TWO webhooks — don't stop at the first

check.complete fires when the search finishes. For a check with potential records that means it arrives carrying status: "pending_adjudication"— a human reviewer hasn't looked at it yet, and result is still null. It is not the answer.

check.adjudicated fires when that review completes, typically within 3 business days, carrying the final result — "clear" if the records were excluded as not your applicant, "consider" if records were released onto the report. Subscribe to it and you never need to poll. A clear check skips this entirely: check.complete is the whole story.

The adverse_action.* events are a separate workflow, not a check-resolution signal — they fire when your organization opens and works an adverse-action case in the dashboard.

Polling a sandbox check

Sandbox checks are simulated and never stored, so a chk_test_... id has no database row behind it. GET /api/v1/checks/{id} still answers for one — the outcome is encoded in the id, so it returns the same deterministic result the POST gave you, with "sandbox": true and creditsCharged: 0. That means you can build and test your polling loop end to end on a test key. A records-result sandbox check reports pending_adjudication with a null result and stays there, exactly as production does until a human finishes the review.

Certified instant check (no SSN)

Run an immediate check on firstName, middleName, lastName, dob, state with no SSN— for when you already hold the subject's signed FCRA disclosure and written authorization. Certify it per request with authorizationOnFile: true and a permissiblePurpose (one of volunteer_screening, employment_screening, youth_serving_organization). middleName is required unless you pass "requireMiddleName": false. SSNs are rejected on this endpoint.

curl -X POST https://www.volunteerbadge.com/api/v1/checks/instant \
  -H "Authorization: Bearer vb_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "firstName": "Jamie", "middleName": "Lee", "lastName": "Rivera",
    "dob": "1981-05-29", "state": "FL",
    "permissiblePurpose": "volunteer_screening",
    "authorizationOnFile": true
  }'

# → { "checkId": "...", "status": "...", "result": "...", "certified": { ... } }

Same screening-scope rule as POST /api/v1/checks: omit includeMisdemeanors / includeTrafficViolations to inherit Settings → Screening; send either flag to override that check only.

Identity verification required

Instant checks via the API — POST /api/v1/checks and POST /api/v1/checks/instant— require your organization's owner to have completed a free, one-time identity verification first. Until then these calls return 403 with code: ID_VERIFICATION_REQUIRED. The owner verifies once in Settings → Account (“Verify my identity”). This applies to every account, and consent-first POST /api/v1/applications invites are never affected.

Errors

StatusMeaning
400Missing or invalid field — see the error message.
401Missing or invalid API key.
402Insufficient credits — buy more in Billing.
403Forbidden — the CRA End-User Agreement must be re-accepted (Settings → API), or (code ID_VERIFICATION_REQUIRED) the org owner must verify their identity before instant checks.
404Resource not found.
429Rate limited — slow down and retry.

Webhooks

Instead of polling, register a URL in Settings → Webhooks and VolunteerBadge will POST to it when things happen. Available events:

EventFires when
check.completeThe search finishes. Clear checks arrive here done (status: complete); a check with potential records arrives as pending_adjudication with a null result and is not finished.
check.adjudicatedA reviewer finishes adjudicating a records-found check (typically within 3 business days). Carries the final result — clear (records excluded) or consider (records released). Subscribe to this and you never have to poll.
check.errorA check fails to process.
application.submittedA volunteer submits their application.
volunteer.createdA new volunteer record is created.
adverse_action.case_openedA report with records is released and VolunteerBadge opens an adverse-action case (or a case is first created for that check).
adverse_action.notice_sentA pre-adverse or final adverse notice is delivered by email (<code>data.stage</code> is <code>pre</code> or <code>final</code>).
adverse_action.dispute_openedThe consumer submits a dispute through the ScreenForge Labs portal link from the pre-adverse notice.
adverse_action.dispute_resolvedScreenForge Labs CRA staff finish reviewing a consumer dispute.
adverse_action.completedThe final adverse notice is sent and the case is complete.

Adverse-action webhooks include caseId, checkId, and volunteerId (null for direct checks). Notice and dispute events also include identifiers like noticeId, stage, or disputeId when available.

Each delivery is a JSON POST with two headers — X-VolunteerBadge-Event (the event name) and X-VolunteerBadge-Signature(an HMAC-SHA256 of the raw body, signed with your endpoint's secret):

POST  (your endpoint)
X-VolunteerBadge-Event: adverse_action.notice_sent
X-VolunteerBadge-Signature: 9f86d081...

{
  "event": "adverse_action.notice_sent",
  "data": {
    "caseId": "...",
    "checkId": "...",
    "volunteerId": "...",
    "stage": "pre",
    "noticeId": "...",
    "deliveryStatus": "delivered"
  },
  "timestamp": "2026-07-18T18:00:00.000Z"
}

Always verify the signature before trusting a payload. In Node:

import crypto from 'crypto';

function verify(rawBody, signatureHeader, secret) {
  const expected = crypto.createHmac('sha256', secret).update(rawBody).digest('hex');
  return crypto.timingSafeEqual(Buffer.from(signatureHeader), Buffer.from(expected));
}

Prefer no-code?

Native one-click connectors are already available for several popular nonprofit platforms (CRM, donor, church-management, and volunteer-management systems) — see the Integrations overview. Until your platform is listed there, the API above — or the spreadsheet importer — covers any sync you need.
Didn't find what you needed? Email support@screenforgelabs.com or browse all articles.