Skip to main content
Email is the Volunteer API’s primary key for Users — the upsert endpoint matches on email, the substring filter on lists is the primary lookup path, and most external-system integrations key off email as the canonical identifier. This workflow page covers the lookup patterns: how to do an exact-email lookup correctly using the substring filter, when to use this versus a blind upsert, and how to handle the edge cases. If you haven’t yet, skim the Users concept page for the field reference and the Create or Update a User workflow for the upsert pattern this lookup often pairs with.

When to use this workflow


The lookup pattern

The Volunteer API has no GET /users?email={exact} endpoint — there’s only email_like, which does a case-insensitive substring match. To do an exact-email lookup, query with email_like and then verify the match explicitly:
JavaScript
Two reasons the secondary equality check matters:

Why email_like and not exact match?

There’s no ?email= (exact) parameter — only ?email_like= (substring). For most email lookups this is fine — a substring of the full email matches uniquely in practice — but the pattern that handles edge cases is to always do a secondary equality check. Three real edge cases to be aware of:

Edge case 1: substring collision

bruce@wayne.example and bruce@wayne.example.com are different addresses but email_like=bruce@wayne.example matches both:
JavaScript
The exact-equality filter selects the right one.

Edge case 2: case sensitivity

JavaScript
The API matches case-insensitively (typical for email matching), but your exact-match check should also be case-insensitive to align. The pattern always lowercases both sides.

Edge case 3: leading/trailing whitespace

User-supplied emails often have stray whitespace from copy-paste. Normalize before the API call:
JavaScript
This is doubly important since the API’s substring match would match " bruce@wayne.example " (with spaces) against bruce@wayne.example. The lookup might “succeed” with the unnormalized input but return surprising data.

When findUserByEmail returns multiple results

Email is expected to be unique per VOMO organization, but defensive code handles the case of multiple matches:
JavaScript
Duplicates shouldn’t exist but can arise from:
  • Migration bugs from a prior system
  • Data import errors
  • The email-change problem where an old record was never reconciled
Detecting and alerting on them is more useful than crashing. The “most recently updated” tiebreaker is a reasonable default but the right resolution typically requires admin team review.

When the lookup spans multiple pages

The email_like substring filter may return more results than fit on one page. For an exact-email lookup, the pattern is to find the match on the first page or accept that more searching is needed:
JavaScript
For most email lookups, the result is on page 1 — the substring filter narrows aggressively. But for very common email substrings, the match may not be on the first page; iterating is the safe pattern.

The find-or-create pattern

Often the use case isn’t “does this user exist?” but “get me this user — create if needed”:
JavaScript

Why use find-or-create vs blind upsert?

The blind upsert handles both cases in one request, which is operationally simpler. Use find-or-create when: For most sync workflows from a system-of-record, blind upsert is the right choice. For partner integrations that defer to VOMO as the source of truth for user data, find-or-create is safer.

A defensive lookup helper

A reference implementation that handles the common gotchas:
JavaScript
The helper:
  • Validates the input is a non-empty string containing an @
  • Normalizes (trim, lowercase) before the API call
  • Iterates pages if needed
  • Filters to exact match
  • Allows opt-in to “return all matches” for defensive workflows
  • Falls back to most-recent-updated on unexpected duplicates

Performance and caching

For partner integrations that look up users frequently, the lookup cost adds up:

When to cache

Cache the lookup result in two places:
JavaScript
A short TTL (5 minutes) is reasonable for production — long enough to avoid redundant lookups in tight loops, short enough that the cache doesn’t drift far from reality. Invalidate when you do an upsert for that email.

When NOT to cache


Common workflow patterns

Pattern 1: route data to an existing user

When external data arrives keyed by email and you need to find the VOMO user to attach it to:
JavaScript
The “no match” case is typically queued for human review rather than ignored — a missing user usually indicates a sync gap that needs investigation.

Pattern 2: precondition for downstream work

JavaScript
Common in workflows where the user is expected to already exist (assigning a verified volunteer to a Group, recording metadata, etc.).

Pattern 3: deduplication before push

For external systems that may attempt to push the same user twice:
JavaScript
This prevents stale external data from overwriting fresher VOMO data — useful when the external system isn’t strictly the source of truth.

Things to watch for

A few subtle issues that surface in production:

email_like matching is sensitive to special characters

Emails with + (e.g., bruce+volunteer@wayne.example) need URL encoding handled correctly. URLSearchParams handles this; manual string concatenation doesn’t:
JavaScript

Email field can be missing or null

Defensive code checks for user.email before calling .toLowerCase():
JavaScript
The ?. optional chaining handles the rare case where a User record has no email (edge cases from data migration, etc.).

Don’t look up by email in tight loops

For workflows that process many records, batching beats per-record lookup:
JavaScript
The bulk-load + in-memory join is one large request instead of N small ones — and the API rate-limit cost is far lower. For accounts of any meaningful size, this is the right pattern.

Where to go next

Create or Update a User

The upsert workflow that pairs naturally with lookup-then-create patterns.

List Users with Filters

The bulk-read workflow useful for in-memory joins.

Users

The reference page with the full field shape and endpoint details.

Sync Users to External System

The end-to-end recipe that uses lookup, upsert, and full sync together.
Last modified on May 22, 2026