The User resource reference — fields, filtering, the list vs. detail shape distinction, the upsert behavior on POST /users, and the practical patterns for finding, creating, and updating volunteer records.
A User in the Volunteer API represents an individual person — typically a volunteer, but also organizers, administrators, and anyone else with a VOMO account. Users are the most commonly accessed resource for partner integrations: they’re queried for reporting, looked up by email for sync workflows, and created or updated when external systems push volunteer data into VOMO.This page covers the User resource in depth — the fields, the three endpoints, the list-vs-detail shape distinction, and the upsert behavior that makes POST /users distinctive.
The UserDetailResource is what you get when you have a specific User and want the full picture. The UserResource is what you get when iterating through many Users for filtering or display.
⚠️ Spec gap: The OpenAPI spec’s UserDetailResource formally documents only participations and profile_field_values (audit #50 confirms the schema is sparse). In practice, the live API likely returns the base UserResource fields alongside these — making UserDetailResource a true superset, not a replacement. Confirm against actual responses for production-critical paths.
⚠️ Spec gap (audit #45, #46): The spec types user_status, membership_status, and membership_role as string with no enum declared. The example values give hints (VERIFIED, ACCEPTED, VOLUNTEER), but the complete set of valid values is not documented.Code that switches on these values should handle unknown values gracefully — log them but don’t crash. The values are stable for known states but new states may be added over time.
All filters use snake_case. The *_like filters are case-insensitive substring matches. The *_before / *_after filters work on the corresponding timestamp fields.
Note the explicit equality check — email_like is a substring match, so a search for bruce@wayne.example might match bruce@wayne.example.com if such an address exists. Confirm with strict equality after the API call.
For workflows that already have the user ID, this is more efficient than filtering the list — one request, full detail, no pagination.
⚠️ Spec gap (audit #50): The spec types data on GET /users/{id} as type: array, but the live API returns a single object. Code should expect an object, not a single-element array.
⚠️ Spec gap (audit #40, #41):hours is typed integer in the spec but returns fractional values (e.g., "4.00"). project_id and project_date_id are typed string but represent integer IDs. Parse hours as a float and the IDs as integers.
Each entry has a field_id (stable identifier), field_label (the human-readable name set in the admin UI), and value (the User’s response).For partner integrations that need to surface or filter on custom data, this is the access path. Look up by field_label for readability or by field_id for stability.
The match is on email. There’s no other way to identify the User for an update — no separate PUT /users/{id} exists.
⚠️ Spec gap (audit #47): The operationId is createUser but the endpoint is functionally an upsert. The spec correctly documents both 200 (updated) and 201 (created) response codes, but the operation name doesn’t reflect the upsert behavior. Future spec revisions may rename this to upsertUser and/or split into separate create and update endpoints.
The response status code distinguishes the two cases:
JavaScript
const { user, created } = await upsertUser({ email: 'bruce@wayne.example', firstName: 'Bruce', lastName: 'Wayne',});if (created) { console.log(`Created new user: ${user.id}`); await sendWelcomeEmail(user);} else { console.log(`Updated existing user: ${user.id}`); // No welcome email — user already exists}
Many integrations care about the distinction — welcome emails should only fire on creation, sync logic may behave differently for new vs. existing users. The status code is the canonical signal.
Required fields for an upsert (typically — confirm against live API for the exact set):
Field
Type
Notes
first_name
string
Required
last_name
string
Required
email
string
Required — used for matching
Optional fields:
Field
Type
Notes
phone
string
Phone number
birthday
string
ISO 8601 date (YYYY-MM-DD)
gender
string
One of M, F, N
role
string
One of VOLUNTEER, ORGANIZER, ADMIN (per audit #46)
address
string
Street address
⚠️ Spec gap (audit #44): The spec’s POST /users request body documents birthday with format: "YYYY-MM-DD" (not a valid OpenAPI format) and gender with format: "M|F|N" (not how enums are typically declared). The intent is clear (date in ISO format; gender enum), but SDK generators may struggle with these. Send ISO-8601 date strings for birthday and the documented enum values for gender.
If the request body fails validation (missing required fields, invalid email format, etc.), the API returns 422 Unprocessable Entity with a structured error response. See Error Handling: 422 Validation Error.
The same upsert call handles both new external records (creates the VOMO User) and updates to existing ones (updates the User). The integration doesn’t need separate code paths.
Volunteer matches Users by email for upsert. Implications:
Email changes break matching. If a User changes their email in your external system, a subsequent upsert will create a new VOMO User rather than update the existing one. Track email history in your integration to handle this case.
Email case is normalized. Searches and matches are case-insensitive. Submit emails in any case; the API handles it.
Email is treated as the canonical identifier. Two records with the same email are considered the same person — there’s no way to have two Users with the same email.