Skip to main content
POST /users is the only write path for Users in the Volunteer API — and it’s an upsert, not a create. The same endpoint handles both “create a new user” and “update an existing user”, with the behavior determined entirely by whether the submitted email matches an existing record. This workflow page covers the upsert in practical detail: when to use it, how to detect whether the API created or updated, how to handle validation errors, and the patterns for the most common upstream scenarios (external system sync, bulk imports, find-or-create flows). If you haven’t yet, skim the Users concept page for the field reference and basic shape of POST /users.

When to use this workflow

The “find or create” framing is intentional. Some workflows benefit from explicit control over create vs. update; this workflow’s 200/201 detection pattern lets you have that control even though the API endpoint is monolithic.

How the upsert works

The matching key is email: The match is case-insensitive. bruce@wayne.example and BRUCE@WAYNE.EXAMPLE resolve to the same user. The match is exact substring match on the full email address — no fuzzy matching, no domain-only matching, no name-based fallback. If the email differs by even one character, the API treats it as a new User.
⚠️ Spec gap (audit #47): The endpoint’s operationId is createUser but its actual behavior is upsert. The spec correctly documents both 200 and 201 response codes, but the operation name doesn’t reflect the upsert reality. A future spec revision may rename this to upsertUser or split it into separate create and update endpoints.

The minimal upsert

cURL
The minimum required fields are typically first_name, last_name, and email. Most other fields are optional but accepted: See the Users concept page for the full request body reference.

Detecting create vs. update

The HTTP status code distinguishes the two outcomes:
JavaScript
The returned created boolean is what downstream code branches on. A common pattern:
JavaScript
Welcome emails should only fire on creation. Audit logs typically benefit from knowing which operation occurred. The 200/201 distinction is the canonical signal.

Scenario 1: Sync from an external system

The most common upsert use case: an external CRM (or HR system, or volunteer management tool) is the source of truth, and changes there should propagate into VOMO.
JavaScript
The pattern handles three outcomes:

Why record the mapping

External systems and VOMO have their own IDs. The mapping table (external_id → vomo_id) lets your integration:
  • Detect “this external record corresponds to this VOMO user”
  • Resolve subsequent updates from external system to the right VOMO User
  • Recover when email changes break the email-based upsert (see The email-change problem below)
JavaScript

Scenario 2: Bulk import

For one-time imports of a volunteer roster — typically when onboarding a new customer or migrating from another system:
JavaScript

Why throttle aggressively for bulk imports

A 10,000-record import at 3 req/sec takes ~55 minutes — a steady pace that avoids triggering rate limits and stays well within the conservative defaults. At 10 req/sec, the same import takes ~17 minutes but risks hitting rate limits and producing inconsistent results. The slower pace is worth it for a one-time operation. See Rate Limits for the broader throttling discussion.

Resumable imports

For large imports, build resumability so a failure partway through doesn’t restart from scratch:
JavaScript
The checkpoint advances incrementally — a crash at record 7,500 of 10,000 lets the next run resume at ~7,500 rather than starting over.

Scenario 3: Find-or-create

When the integration wants explicit control over which case occurred — and is willing to do an extra lookup to be certain:
JavaScript
This pattern is preferred when:
  • The integration shouldn’t update existing users blindly (e.g., the external system has stale data)
  • The create case has expensive side effects (welcome emails, provisioning, etc.) that you want to make absolutely sure happen only once
  • You want a defensive logging/auditing trail of which case occurred
The cost: an extra lookup before the upsert. For most workflows, the upsert-with-detection pattern from Scenario 1 is enough. Use find-or-create when the explicit branching matters. See the dedicated Find a User by Email workflow for the lookup pattern.

Handling validation errors

When the request body fails server-side validation, the API returns 422 Unprocessable Entity with a structured error body:
Handle these with a structured error class:
JavaScript
The field-level errors come back keyed by the API’s field name (email, first_name, etc.). If your UI uses different field names, build a translation layer:
JavaScript
See Error Handling for the broader error classification.

The email-change problem

The most subtle reality of email-based upsert: if a user’s email changes in your external system, a subsequent upsert creates a new VOMO user rather than updating the existing one.

The scenario

  1. Bruce Wayne signs up in your external system as bruce@wayne.example
  2. You upsert into VOMO → creates VOMO user #12345
  3. Bruce updates his email to bruce.wayne@wayne.example in your external system
  4. You upsert into VOMO → creates a new VOMO user #99999 (the email doesn’t match #12345)
You now have two VOMO records for the same person. The original (#12345) still has the old email; the new one (#99999) has the updated email but no participation history, no group memberships, no profile data.

The mitigation

Track external IDs in your external-side mapping table, not in VOMO. When an email changes:
JavaScript
The right resolution for an email change typically requires human review — the admin team merges the records in the VOMO admin UI, or coordinates with VOMO support for a programmatic merge.

Why not “just push the new email”

Because the upsert matches on email, pushing bruce.wayne@wayne.example for a user previously known by bruce@wayne.example creates a new user rather than updating the old one. The old user’s email isn’t updated — there’s just a new record alongside it. For partner integrations to update an existing user’s email, the typical path is:
  1. Detect the email change in your external system
  2. Pause sync for this user until resolved
  3. Coordinate with the customer’s admin team to update the email in the VOMO admin UI
  4. Update your external-side mapping table to reflect the new email
  5. Resume sync
It’s a friction point, but the alternative (silent duplicate creation) is worse.

What can’t be done via the API

For the merge case especially, coordinate with the customer’s admin team — the VOMO admin UI has merge tools that aren’t exposed in the API.

A reference upsert client

A minimal, well-organized client:
JavaScript
The _buildBody method strips out empty optional fields — preventing the API from interpreting an empty string as “set this field to empty.”

Where to go next

Find a User by Email

The lookup workflow that pairs with this upsert pattern.

List Users with Filters

The bulk-read workflow for incremental sync.

Users

The reference page for User fields and endpoints.

Sync Users to External System

The end-to-end recipe combining this upsert with broader sync architecture.
Last modified on May 22, 2026