> ## Documentation Index
> Fetch the complete documentation index at: https://docs.virtuous.org/llms.txt
> Use this file to discover all available pages before exploring further.

# The Volunteer Data Model

> The eight resource families in the Volunteer API, how they relate, and the central Participation pattern that ties Users to specific Project Dates.

The Volunteer API's data model is built around volunteer engagement — Users who participate in Projects on specific Dates, organized into Groups, attached to Campaigns, owned by Organizations. This page walks through the eight resource families, how they relate, and the central Participation pattern that's unique to volunteer management.

The audience is partner integration engineers and architects building or designing against the Volunteer API. Understanding this model first prevents a lot of confusion downstream.

## The eight resource families

```mermaid theme={null}
graph TD
  Org["Organization<br/>(parent-child hierarchy)"]
  Project["Project<br/>(volunteer opportunity template)"]
  ProjectDate["Project Date<br/>(specific occurrence)"]
  Participation["Participation<br/>(volunteer's attendance record)"]
  User["User<br/>(volunteer)"]
  Group["Group<br/>(volunteer grouping)"]
  Campaign["Campaign<br/>(initiative spanning Projects)"]
  Form["Form<br/>(data-collection)"]
  Certificate["Certificate<br/>(training/achievement)"]

  Org -->|owns| Project
  Org -->|owns| Group
  Org -->|owns| Campaign
  Org -->|children| Org
  Campaign -->|contains| Project
  Project -->|has schedule| ProjectDate
  ProjectDate -->|generates| Participation
  User -->|attends| Participation
  User -->|belongs to| Group
  User -->|earns| Certificate
  User -->|completes| Form
  Project -->|attaches| Form
```

The eight resource families:

| Family            | What it represents                                        | Read endpoint                                                                           | Write endpoints                                                                       |
| ----------------- | --------------------------------------------------------- | --------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------- |
| **User**          | A volunteer (or anyone with a VOMO account)               | `GET /users`, `GET /users/{id}`                                                         | `POST /users` (upsert)                                                                |
| **Project**       | A volunteer opportunity template (the recurring "what")   | `GET /projects`, `GET /projects/{id}`                                                   | `POST /projects`, `PUT /projects/{id}`                                                |
| **Project Date**  | A specific scheduled occurrence of a Project (the "when") | `GET /projects/date/{id}`, `GET /projects/today`                                        | None                                                                                  |
| **Participation** | A record of a User attending a Project Date               | Embedded in `UserDetailResource` and Project Date responses                             | None                                                                                  |
| **Group**         | A grouping of Users (team, affinity, etc.)                | `GET /groups`, `GET /groups/{id}`, `GET /groups/{id}/members`                           | `POST /groups`, `PUT /groups/{id}`, `DELETE /groups/{id}`, `PUT /groups/{id}/members` |
| **Campaign**      | An umbrella initiative spanning multiple Projects         | `GET /campaigns`, `GET /campaigns/{id}`                                                 | None                                                                                  |
| **Organization**  | The customer's organization (with parent-child hierarchy) | `GET /organizations`, `GET /organizations/{id}`                                         | None                                                                                  |
| **Form**          | A data-collection form attached to a Project              | `GET /forms`, `GET /forms/{id}/completions`, `GET /forms/{id}/completions/{completion}` | None                                                                                  |
| **Certificate**   | A training credential or achievement                      | `GET /certificates`                                                                     | None                                                                                  |

Two patterns to notice from this table:

| Pattern                     | Implication                                                                                                                                      |
| --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
| **Read-heavy**              | 20 of 24 endpoints are reads. The API is designed primarily for data extraction.                                                                 |
| **No Participation writes** | Participations are recorded through the VOMO admin UI (not via API). Partner integrations can read them but cannot create them programmatically. |

***

## The central pattern: User ↔ Participation ↔ Project Date

The most important relationship in the model is the **Participation triangle**:

```mermaid theme={null}
graph LR
  User["User"]
  Participation["Participation<br/>(record)"]
  ProjectDate["Project Date<br/>(specific occurrence)"]
  Project["Project<br/>(template)"]

  User -->|"recorded as"| Participation
  Participation -->|"on"| ProjectDate
  ProjectDate -->|"scheduled for"| Project
```

When a volunteer (User) participates in a specific scheduled occurrence (Project Date) of a recurring opportunity (Project), a Participation record is created. The Participation captures:

| Field             | What it captures                                      |
| ----------------- | ----------------------------------------------------- |
| `signed_up_at`    | When the volunteer registered for the Project Date    |
| `checked_in_at`   | When they actually arrived                            |
| `checked_out_at`  | When they left                                        |
| `hours`           | Total time volunteered (fractional — e.g., 2.5 hours) |
| `verified`        | Whether the organizer confirmed the participation     |
| `role`            | What role they played (Volunteer, Team Lead, etc.)    |
| `guests`          | How many additional people they brought               |
| `project_id`      | The Project the participation is for                  |
| `project_date_id` | The specific Project Date                             |

Participations are how the API answers questions like:

| Question                                                     | How Participation answers it                                      |
| ------------------------------------------------------------ | ----------------------------------------------------------------- |
| "How many hours has Bruce volunteered this year?"            | Sum `hours` across his participations with `signed_up_at` in 2025 |
| "Who's confirmed for the food bank shift on Saturday?"       | List participations with that `project_date_id`                   |
| "Has this user shown up for any project they signed up for?" | Compare `checked_in_at` to `signed_up_at`                         |
| "Which users are repeat volunteers?"                         | Group participations by `user_id`, count distinct `project_id`    |

### Reading participations

There's no top-level `GET /participations` endpoint. Participations are always returned **embedded** in another resource's response:

| Where participations appear              | Endpoint                                                      |
| ---------------------------------------- | ------------------------------------------------------------- |
| As an array on a user's detail response  | `GET /users/{id}` returns `UserDetailResource.participations` |
| As participant entries on a Project Date | `GET /projects/date/{id}` includes participants               |
| Indirectly visible in Project responses  | Through participant counts and similar aggregates             |

For partner integrations that need a flat "all participations" view, the typical pattern is:

1. List all users via `GET /users` with pagination.
2. For each user, `GET /users/{id}` to get their participations.
3. Flatten across all users.

This is N+1 — one initial query plus one per user. For large customers, it's expensive. See [API Performance Tips](/volunteer/best-practices/api-performance-tips) for the optimization patterns.

<Warning>
  **⚠️ Spec gap (audit #40, #41):** The `ParticipationResource` schema types `hours` as `integer` and `project_id` / `project_date_id` as `string`. The live API returns fractional hours (e.g., `"4.00"`) and integer IDs. Code should parse `hours` as a float and IDs as integers regardless of the spec's type declaration.
</Warning>

***

## Project vs. Project Date

A common source of confusion: the difference between a **Project** and a **Project Date**.

### Project = the template (the "what")

A Project is the volunteer opportunity itself — what it is, what's being asked of volunteers, what skills are needed, what the organization wants to accomplish. It's a long-lived record that persists across many actual occurrences.

```text theme={null}
Project: "Saturday Food Bank Volunteer Shift"
  • Description: Sort donated food, pack boxes for delivery
  • Address: 123 Charity Lane
  • Skills needed: Lifting up to 25 lbs
  • Age limit: 16+
  • Privacy: Public
  • [recurring schedule — runs many Saturdays]
```

The Project has fields like `name`, `description`, `organization`, `address`, `age_limit`, `privacy`, `categories`, and so on.

### Project Date = the specific occurrence (the "when")

A Project Date is one specific scheduled occurrence of a Project. The same Project can have dozens or hundreds of Project Dates over its lifetime.

```text theme={null}
Project Date #1011: occurrence of "Saturday Food Bank Volunteer Shift"
  • Starts: 2025-03-15 09:00
  • Ends: 2025-03-15 13:00
  • Participant count: 15
  • [participations attached]
```

The Project Date has fields like `id`, `starts_at`, `ends_at`, `participant_count`, and the embedded participants for that date.

### In the API

| Endpoint                  | What it returns                                         |
| ------------------------- | ------------------------------------------------------- |
| `GET /projects/{id}`      | The Project (template) with all metadata                |
| `GET /projects/date/{id}` | A specific Project Date with its participants           |
| `GET /projects/today`     | All Project Dates happening today (across all Projects) |

<Note>
  **⚠️ Spec gap (audit #34):** The endpoint `GET /projects/date/{id}` uses a verb-like singular noun (`date`) in the path, which is inconsistent with REST conventions. The audit recommends renaming to `GET /projects/{projectId}/dates/{dateId}`. The current path is what works against the live API today; the alternative path may exist in a future v2.
</Note>

### Why this matters for integrations

A common bug: querying `/projects/{id}` expecting to get the participants for an upcoming shift, then being surprised that it returns the template's metadata instead. The participants are on the Project Date, not the Project.

| Goal                                           | Endpoint                      |
| ---------------------------------------------- | ----------------------------- |
| Get the opportunity's description and policies | `GET /projects/{id}`          |
| Get who's signed up for a specific shift       | `GET /projects/date/{dateId}` |
| Get all shifts happening today                 | `GET /projects/today`         |

***

## Organization hierarchy: the "organization family"

Organizations in VOMO can have parent-child relationships. The customer's primary Organization may have children — sub-organizations like local chapters, regional offices, or affiliated nonprofits — and may itself be a child of a larger umbrella organization.

```mermaid theme={null}
graph TD
  Parent["Wayne Foundation<br/>(parent)"]
  Child1["Gotham Outreach<br/>(child)"]
  Child2["Wayne Manor Programs<br/>(child)"]
  Child3["Wayne Medical<br/>(child)"]

  Parent --> Child1
  Parent --> Child2
  Parent --> Child3
```

### What this enables

* A single API token can be scoped to access the parent organization and its children.
* Reports and aggregations span the full organization family by default.
* Projects, Campaigns, and Groups are owned by specific organizations within the family.

### What the API exposes

| Endpoint                  | What it returns                                           |
| ------------------------- | --------------------------------------------------------- |
| `GET /organizations`      | The family of organizations the token has access to       |
| `GET /organizations/{id}` | A single organization with its parent/child relationships |

<Warning>
  **⚠️ Spec gap (audit #25, #26, #27):** The Organization endpoints have empty `schema: {}` in the spec — the response shape is documented only through inline examples. There is no `OrganizationResource` component schema. The "organization family" concept is mentioned in the endpoint summary but isn't formally documented. Build parsers from the actual response shapes.
</Warning>

### Per-resource organization attribution

Most resources track which organization in the family they belong to:

| Resource | Org-attribution fields                                          |
| -------- | --------------------------------------------------------------- |
| Project  | `organization`, `organization_id`, `organization_slug`          |
| Group    | `parent_id` (the parent group, not the parent organization)     |
| Campaign | Filterable by `org_slug` query parameter                        |
| User     | `membership_status`, `membership_role` (within the current org) |

When the partner integration is reading data, the attribution lets them route or filter records to the right organization within the family.

***

## Forms: data-collection attached to Projects

Forms in VOMO are data-collection templates — waivers, signups, profile fields, application questions. Volunteers complete Forms; the completion is captured as a Form Completion.

```mermaid theme={null}
graph LR
  Form["Form<br/>(template)"]
  Field["Form Fields<br/>(questions)"]
  Completion["Form Completion<br/>(volunteer's answers)"]
  User["User"]

  Form --> Field
  Form --> Completion
  Completion --> User
```

The relationships:

| Resource                | Description                                                             |
| ----------------------- | ----------------------------------------------------------------------- |
| **Form**                | The template — questions, validation rules, field types                 |
| **Form Field**          | An individual question within a Form (with options for dropdowns, etc.) |
| **Form Completion**     | A volunteer's specific submission of a Form, with their answers         |
| **Form Field Response** | A volunteer's answer to a specific field within a Completion            |

### Where Forms live

| Endpoint                                     | What it returns                              |
| -------------------------------------------- | -------------------------------------------- |
| `GET /forms`                                 | All Forms in the customer's organization     |
| `GET /forms/{id}/completions`                | All Completions of a specific Form           |
| `GET /forms/{id}/completions/{completionId}` | A single Completion with all field responses |

Forms are attached to Projects in the admin UI; the partner integration reads them through the endpoints above. Common use cases include waivers (signed once per volunteer), event-specific signups (additional info beyond standard Project signup), and post-event surveys.

### Form types

The `form_type` field on `FormResource` has documented enum values, though the audit (#54) flags that only `"PROJECT"` is documented and other types likely exist.

***

## Certificates: training and achievements

Certificates represent training credentials, badges, or other achievements that volunteers earn. Common uses include:

* Required safety training (e.g., food handler certification for food bank volunteers)
* Background check completion
* Specialized role qualifications (e.g., team leader training)
* Completion badges for milestone hours

```mermaid theme={null}
graph LR
  Certificate["Certificate<br/>(training/credential)"]
  User["User"]

  User -->|"earns"| Certificate
```

| Endpoint            | What it returns                                 |
| ------------------- | ----------------------------------------------- |
| `GET /certificates` | All Certificates in the customer's organization |

<Note>
  **⚠️ Spec gap (audit #14):** The same resource is referred to as "certificate", "certification", and "certificates" in different places in the spec — the path is `/certificates`, the tag is `Certifications`, the operationId is `listCertifications`, and the schema is `CertificateResource`. The naming will be reconciled in a future spec revision. For partner integrations today, use the path (`/certificates`) and the schema name (`CertificateResource`) as your reference points.
</Note>

Certificate earning is recorded per-user; the User Detail response (`GET /users/{id}`) typically includes earned certificates alongside other user data.

***

## Groups: cross-cutting User collections

Groups in VOMO organize Users into collections — teams, affinity groups, chapter members, etc. Unlike Projects (which represent opportunities), Groups represent stable collections of people that persist independent of specific events.

```mermaid theme={null}
graph LR
  Group["Group<br/>(team or affinity)"]
  Member["Group Member<br/>(User in this Group)"]
  User["User"]

  User -->|"belongs to"| Member
  Member -->|"member of"| Group
  Group -->|"may have"| Group
```

The relationships:

| Resource         | Description                                                  |
| ---------------- | ------------------------------------------------------------ |
| **Group**        | A named collection of Users                                  |
| **Group Member** | The relationship between a User and a Group                  |
| **Parent Group** | A Group can have a parent Group (for hierarchical groupings) |

Groups have the most write-heavy surface of any Volunteer resource:

| Endpoint                   | What it does                                               |
| -------------------------- | ---------------------------------------------------------- |
| `GET /groups`              | List Groups (filterable by `parent_id`, `name_like`, etc.) |
| `POST /groups`             | Create a Group                                             |
| `GET /groups/{id}`         | Get a Group's details                                      |
| `PUT /groups/{id}`         | Update a Group (full replacement)                          |
| `DELETE /groups/{id}`      | Delete a Group                                             |
| `GET /groups/{id}/members` | List the Users in a Group                                  |
| `PUT /groups/{id}/members` | Replace the Group's member list                            |

This makes Groups the canonical "build a volunteer team programmatically" resource. Common patterns include:

* Syncing external team rosters into VOMO Groups
* Building Groups based on participation criteria (e.g., "active volunteers in 2024")
* Creating event-specific Groups for managing rosters

<Warning>
  **⚠️ Spec gap (audit #21, #22):** `POST /groups` returns `200` (not `201` per HTTP convention for resource creation). `DELETE /groups/{id}` returns `200` (not `204` per convention for empty-body deletes). These are spec-confirmed behaviors; integrations should code for what the API actually returns, not what HTTP conventions suggest.
</Warning>

***

## Campaigns: longer-running initiatives

Campaigns group multiple related Projects under a single umbrella initiative. They represent the customer's longer-term goals or themes:

```text theme={null}
Campaign: "Summer Outreach 2025"
  ├── Project: "June Food Drive"
  ├── Project: "July Park Cleanup"
  └── Project: "August Volunteer Fair"
```

| Endpoint              | What it returns                                             |
| --------------------- | ----------------------------------------------------------- |
| `GET /campaigns`      | All Campaigns (filterable by `org_slug`, `name_like`, etc.) |
| `GET /campaigns/{id}` | A single Campaign with embedded Project list                |

Campaigns are read-only from the API — they're managed in the VOMO admin UI. Partner integrations use them for reporting and attribution ("how is the Summer Outreach campaign performing?").

<Note>
  **⚠️ Spec gap (audit #4, #13):** The Campaign endpoints have empty `schema: {}` in the spec, and `CampaignResource` description erroneously reads "A VOMO Project." Build parsers from actual response shapes.
</Note>

***

## What's NOT in the API

Several resource types that exist in VOMO but aren't currently exposed in the API:

| Resource                          | Why partner integrations should know                                                         |
| --------------------------------- | -------------------------------------------------------------------------------------------- |
| **Participation creation/update** | Volunteer hours are recorded in the admin UI; no API path to create or modify participations |
| **Project Date scheduling**       | Project Dates are managed in the admin UI; no API path to create or modify them              |
| **Form submission**               | Volunteers complete Forms through the VOMO UI; no API path to submit a Form response         |
| **Certificate awarding**          | Certificates are awarded in the admin UI                                                     |
| **Volunteer time tracking**       | Live clock-in/clock-out happens in the admin UI                                              |

For partner integrations needing to record participations programmatically, coordinate with VOMO's admin team for alternative paths — typically through Group management or scheduled imports.

See [Understand Write Limitations](/volunteer/workflows/understand-write-limitations) for the complete picture.

***

## ID stability and matching

Across the API, several patterns govern how IDs work:

| ID type              | Stability                 | Notes                                                   |
| -------------------- | ------------------------- | ------------------------------------------------------- |
| `user.id`            | Stable                    | The primary identifier for a User across all references |
| `project.id`         | Stable                    | The primary identifier for a Project (the template)     |
| `project_date.id`    | Stable                    | The primary identifier for a specific occurrence        |
| `group.id`           | Stable                    | The primary identifier for a Group                      |
| `campaign.id`        | Stable                    | The primary identifier for a Campaign                   |
| `organization.id`    | Stable                    | The primary identifier for an Organization              |
| `organization.slug`  | Stable but human-readable | Used for filtering (e.g., `?org_slug=wayne-foundation`) |
| `form_completion.id` | Stable                    | One per submission                                      |

User matching for upsert (`POST /users`) is done by **email** — submitting a user with an existing email updates that user; submitting with a new email creates a new one. See [Users: upsert behavior](/volunteer/concepts/users#upsert-behavior).

***

## Cross-API mapping

For partners integrating Volunteer alongside CRM+ or Raise, the conceptual mappings:

| Volunteer concept | CRM+ equivalent                  | Raise equivalent      |
| ----------------- | -------------------------------- | --------------------- |
| User              | Contact                          | Donor                 |
| Participation     | Activity (volunteer activity)    | (no equivalent)       |
| Project Date      | Activity (event)                 | (no equivalent)       |
| Campaign          | Communication                    | Campaign              |
| Group             | Communication audience / Segment | Segment               |
| Organization      | (organization itself)            | (organization itself) |

Note these are conceptual analogs, not exact matches. A Volunteer User is roughly equivalent to a CRM+ Contact in role (the person record), but the field shapes and write paths differ significantly.

For partner integrations syncing across all three APIs, the typical pattern is:

* Match by email when joining records across APIs
* Treat each API's records as authoritative for their own domain (User in Volunteer, Contact in CRM+, Donor in Raise)
* Use partner-side mapping tables when the same person needs distinct records in each system

See the Volunteer **Recipes** for examples of cross-API integration patterns.

***

## Where to go next

<CardGroup cols={2}>
  <Card title="Users" icon="user" href="/volunteer/concepts/users">
    The User resource reference in depth — fields, filters, upsert behavior.
  </Card>

  <Card title="Projects and Project Dates" icon="calendar-days" href="/volunteer/concepts/projects-and-project-dates">
    The Project (template) vs. Project Date (occurrence) distinction in full detail.
  </Card>

  <Card title="Groups" icon="users" href="/volunteer/concepts/groups">
    The most write-heavy resource — Group management and members.
  </Card>

  <Card title="Understand Write Limitations" icon="ban" href="/volunteer/workflows/understand-write-limitations">
    The explicit list of what the API can and can't do.
  </Card>
</CardGroup>
