> ## 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.

# Projects & Project Dates

> The Project (template) vs. Project Date (occurrence) distinction in detail — fields, endpoints, the Happening pattern, and the practical patterns for reading and managing volunteer opportunities.

Projects and Project Dates are two distinct concepts that often get confused. A **Project** is the template — the volunteer opportunity itself, describing what's being done, where, by whom, with what policies. A **Project Date** is one specific scheduled occurrence — Saturday March 15, 9am to 1pm, at this address, with these participants.

This page covers both resources in detail: the field shapes, the endpoints, the relationship between them, and the practical patterns for reading and managing them.

## Project vs. Project Date — the mental model

The clearest analogy is a **recurring meeting**:

```text theme={null}
Project (the recurring meeting series)
  ├── Project Date #1011 (Saturday March 15, 2025)
  ├── Project Date #1012 (Saturday March 22, 2025)
  ├── Project Date #1013 (Saturday March 29, 2025)
  └── Project Date #1014 (Saturday April 5, 2025)
```

The Project carries the long-lived information — name, description, address, eligibility rules, designated organizers. The Project Date carries the per-occurrence information — start time, end time, participants who actually showed up.

| Aspect                | Project                                      | Project Date                                    |
| --------------------- | -------------------------------------------- | ----------------------------------------------- |
| **Persistence**       | Long-lived (months or years)                 | Short-lived (a few hours typically)             |
| **Identity**          | The opportunity ("Saturday Food Bank Shift") | The occurrence ("March 15, 2025 shift")         |
| **Has participants?** | No — participants are on dates               | Yes — the embedded participants are on the date |
| **Created by**        | Customer's organizer                         | Schedule generates them from the Project        |
| **API write surface** | `POST /projects`, `PUT /projects/{id}`       | None — managed in admin UI                      |

***

## The Project endpoints

| Endpoint              | Method | What it does                          |
| --------------------- | ------ | ------------------------------------- |
| `/projects`           | GET    | List Projects (paginated, filterable) |
| `/projects`           | POST   | Create a Project                      |
| `/projects/{id}`      | GET    | Get a Project's full detail           |
| `/projects/{id}`      | PUT    | Update a Project (full replacement)   |
| `/projects/today`     | GET    | List Project Dates happening today    |
| `/projects/date/{id}` | GET    | Get a specific Project Date           |

Two paths in this list relate to Project Dates (`/projects/today` and `/projects/date/{id}`) rather than Projects themselves. The next sections distinguish them.

<Warning>
  **⚠️ Spec gap (audit #34):** The endpoint `GET /projects/date/{id}` has an unconventional path (verb-like singular noun in the middle). The audit recommends renaming to `GET /projects/{projectId}/dates/{dateId}` in a future spec revision. For now, use the documented `/projects/date/{id}` path.
</Warning>

***

## The Project resource

### List shape (`ProjectResource`)

`GET /projects` returns an array of `ProjectResource` objects — the metadata describing the volunteer opportunity:

| Field               | Type    | Description                                       |
| ------------------- | ------- | ------------------------------------------------- |
| `type`              | string  | `"project"` — VOMO object type                    |
| `id`                | integer | The Project's stable ID                           |
| `name`              | string  | The Project's display name                        |
| `project_name`      | string  | **Duplicate of `name`** — see warning below       |
| `description`       | string  | The Project's description                         |
| `url`               | string  | A URL to the Project's page in the VOMO UI        |
| `organization`      | string  | The Organization name                             |
| `organization_id`   | integer | The Organization ID                               |
| `organization_slug` | string  | The Organization's slug (URL-safe identifier)     |
| `updated_at`        | string  | ISO 8601 datetime                                 |
| `created_at`        | string  | ISO 8601 datetime                                 |
| `published_at`      | string  | When the Project was published (or null if draft) |
| `images`            | array   | Project images                                    |
| `campaigns`         | array   | Campaigns this Project is attached to             |
| `owners`            | array   | Project organizers/owners                         |
| `address`           | object  | The Project's location                            |
| `certificates`      | array   | Certificates required for this Project            |
| `form_completions`  | array   | Forms attached to this Project                    |

<Warning>
  **⚠️ Spec gap (audit #42):** `ProjectResource` defines both `name` AND `project_name` as separate properties with identical descriptions. This is a duplicate field — consumers can't know which to use. Use `name` (which aligns with other Virtuous APIs); treat `project_name` as a deprecated alias likely to be removed.
</Warning>

<Warning>
  **⚠️ Spec gap (audit #43):** `ProjectResource.address` is typed `array` in the spec, but an address is conceptually a single object (street, city, state, etc.), not a collection. The live API likely returns an object; code should parse it as such regardless of the spec's declaration.
</Warning>

### Detail shape (`ProjectDetailResource`)

`GET /projects/{id}` returns a `ProjectDetailResource` — a superset of `ProjectResource` plus the operational policy fields:

| Field                           | Type    | Description                                     |
| ------------------------------- | ------- | ----------------------------------------------- |
| `point_person`                  | object  | Contact for the Project day                     |
| `details`                       | string  | Operational notes (what to wear, what to bring) |
| `allow_guests`                  | boolean | Can volunteers bring guests?                    |
| `age_limit`                     | integer | Minimum volunteer age                           |
| `volunteer_question`            | string  | Optional question asked during signup           |
| `participant_approval_required` | boolean | Do organizers approve signups?                  |
| `background_check_required`     | boolean | Background check required to participate?       |
| `show_volunteer_counter`        | boolean | Display volunteer count publicly?               |
| `volunteer_counter_threshold`   | integer | Show counter only above this count              |
| `draft`                         | boolean | Is the Project in draft (not yet published)?    |
| `privacy`                       | string  | `"PUBLIC"` or `"PRIVATE"`                       |
| `all_dates`                     | array   | All scheduled Project Dates for this Project    |
| `next_date`                     | object  | The next upcoming Project Date                  |

The `all_dates` and `next_date` fields are particularly useful for partner integrations — they let you read the Project's schedule without separate Project Date fetches.

***

## Listing projects

```bash cURL theme={null}
curl "https://api.vomo.org/v1/projects?page=1&active=true&published=true" \
  -H "Authorization: Bearer $VOMO_API_TOKEN"
```

### Available filters

| Parameter        | What it filters by                                                                    |
| ---------------- | ------------------------------------------------------------------------------------- |
| `page`           | Page number (default 1)                                                               |
| `active`         | `true` or `false` — limit to active or inactive Projects                              |
| `published`      | `true` or `false` — limit to published or draft Projects                              |
| `anytime`        | `true` or `false` — include/exclude "anytime" Projects (those without specific dates) |
| `org_slug`       | Filter by Organization slug (comma-separated for multiple)                            |
| `name_like`      | Substring match against Project name                                                  |
| `created_before` | Projects created on or before a date                                                  |
| `created_after`  | Projects created on or after a date                                                   |
| `updated_before` | Projects updated on or before a date                                                  |
| `updated_after`  | Projects updated on or after a date                                                   |
| `dates_before`   | Projects with dates that started at or before this datetime                           |
| `dates_after`    | Projects with dates that started at or after this datetime                            |

The `dates_before` and `dates_after` filters are distinctive — they filter Projects by the timing of their Project Dates, not by the Project's own creation or update time. Useful for "Projects with shifts coming up this month" queries.

### Common list patterns

**Currently active and published projects:**

```javascript JavaScript theme={null}
async function activePublishedProjects() {
  const params = new URLSearchParams({
    active: 'true',
    published: 'true',
  });

  return paginate(`https://api.vomo.org/v1/projects?${params}`);
}
```

**Projects with shifts in the next 30 days:**

```javascript JavaScript theme={null}
async function upcomingProjects() {
  const now = new Date();
  const thirtyDaysOut = new Date(now.getTime() + 30 * 24 * 60 * 60 * 1000);

  const params = new URLSearchParams({
    active: 'true',
    published: 'true',
    dates_after: now.toISOString(),
    dates_before: thirtyDaysOut.toISOString(),
  });

  return paginate(`https://api.vomo.org/v1/projects?${params}`);
}
```

**Projects for a specific organization within the family:**

```javascript JavaScript theme={null}
async function projectsForChildOrg(orgSlug) {
  const params = new URLSearchParams({ org_slug: orgSlug });
  return paginate(`https://api.vomo.org/v1/projects?${params}`);
}
```

***

## Fetching a single project

```bash cURL theme={null}
curl https://api.vomo.org/v1/projects/789 \
  -H "Authorization: Bearer $VOMO_API_TOKEN"
```

Returns a `ProjectDetailResource` with full policy fields and scheduled dates.

```javascript JavaScript theme={null}
async function getProjectDetail(projectId) {
  const response = await fetch(
    `https://api.vomo.org/v1/projects/${projectId}`,
    { headers: { Authorization: `Bearer ${token}` } }
  );

  if (response.status === 404) return null;
  if (!response.ok) throw new Error(`Failed: ${response.status}`);

  const result = await response.json();
  return parseProjectDetail(result.data);
}
```

The detail response is useful when you need policy info (age limit, background check requirement) along with the schedule.

***

## Creating a project

```bash cURL theme={null}
curl -X POST https://api.vomo.org/v1/projects \
  -H "Authorization: Bearer $VOMO_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Saturday Food Bank Volunteer Shift",
    "description": "Sort donated food, pack boxes for delivery",
    "address": {
      "street": "123 Charity Lane",
      "city": "Gotham",
      "state": "NJ",
      "postal_code": "10001"
    },
    "age_limit": 16,
    "allow_guests": true,
    "draft": false
  }'
```

<Warning>
  **⚠️ Spec gap (audit #31, #32, #33):** The `POST /projects` request body is defined as an anonymous inline `object` in the spec (not a named schema), and `PUT /projects/{id}` contains many field descriptions that are literal placeholder text (`"Update Project"`). The exact valid field set, validation rules, and create vs. update field differences aren't well-documented.

  For production use, confirm the field set against the live API by inspecting actual create responses or by coordinating with VOMO support.
</Warning>

A successful create returns the new Project. Capture the `id` for subsequent updates.

***

## Updating a project

```bash cURL theme={null}
curl -X PUT https://api.vomo.org/v1/projects/789 \
  -H "Authorization: Bearer $VOMO_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Saturday Food Bank Volunteer Shift",
    "description": "Updated description with new details",
    "age_limit": 18,
    /* ... all other fields ... */
  }'
```

`PUT /projects/{id}` is a **full replacement** — the request body must contain every field that should persist. Fields omitted from the request may be set to default values or null.

### The GET-then-PUT pattern

For partial updates, fetch the current record, modify the fields you need, then PUT the full record back:

```javascript JavaScript theme={null}
async function updateProjectDescription(projectId, newDescription) {
  // 1. Fetch current state
  const current = await getProjectDetail(projectId);
  if (!current) throw new Error('Project not found');

  // 2. Apply changes
  const updated = {
    ...current,
    description: newDescription,
  };

  // 3. PUT the full record
  const response = await fetch(
    `https://api.vomo.org/v1/projects/${projectId}`,
    {
      method: 'PUT',
      headers: {
        Authorization: `Bearer ${token}`,
        'Content-Type': 'application/json',
      },
      body: JSON.stringify(updated),
    }
  );

  if (!response.ok) throw new Error(`Update failed: ${response.status}`);
  return response.json();
}
```

The pattern is common in REST APIs without PATCH support. The cost: an extra read per update. For high-frequency update workloads, this can add up — see [API Performance Tips](/volunteer/best-practices/api-performance-tips) for caching patterns.

***

## Project Date endpoints

Two endpoints expose Project Dates:

| Endpoint                  | What it returns                                           |
| ------------------------- | --------------------------------------------------------- |
| `GET /projects/today`     | All Project Dates happening today across the organization |
| `GET /projects/date/{id}` | A specific Project Date with its participants             |

There's **no `GET /project-dates/{id}` or similar** — Project Dates are accessed through the Project path or through "today" view only.

### `GET /projects/today`

Returns a list of `HappeningResource` objects — Project Dates scheduled for today:

```json theme={null}
{
  "data": [
    {
      "type": "happening",
      "id": 1011,
      "starts_at": "2025-03-15T09:00:00Z",
      "ends_at": "2025-03-15T13:00:00Z",
      "participant_count": 15
    },
    {
      "type": "happening",
      "id": 1012,
      "starts_at": "2025-03-15T14:00:00Z",
      "ends_at": "2025-03-15T18:00:00Z",
      "participant_count": 8
    }
  ]
}
```

This is the "what's happening today" feed — useful for daily-summary dashboards, day-of-event reports, and check-in tools.

<Note>
  The VOMO term **"Happening"** is used internally for a Project Date. `HappeningResource` is the schema; Project Date is the conceptual name. Both refer to the same thing.
</Note>

### `GET /projects/date/{id}`

Returns a specific Project Date with full detail including the participants:

```javascript JavaScript theme={null}
async function getProjectDate(projectDateId) {
  const response = await fetch(
    `https://api.vomo.org/v1/projects/date/${projectDateId}`,
    { headers: { Authorization: `Bearer ${token}` } }
  );

  if (response.status === 404) return null;
  if (!response.ok) throw new Error(`Failed: ${response.status}`);

  return response.json();
}
```

<Warning>
  **⚠️ Spec gap (audit #4):** The `GET /projects/date/{id}` response uses empty `schema: {}` in the spec. The response shape is documented only through inline examples — the exact field set is not formally specified. Build parsers from the actual response shape.
</Warning>

### Participants on a Project Date

The participants embedded in a Project Date response use the `Participant` schema:

| Field          | Type   | Description                                   |
| -------------- | ------ | --------------------------------------------- |
| `full_name`    | string | The participant's full name                   |
| `display_name` | string | The display name (often includes guest count) |
| `guest`        | array  | The participant's guests for this date        |

`Participant` is the display representation; `ParticipationResource` (returned on `UserDetailResource.participations`) is the record representation with timing and verification details. The two are related but distinct.

***

## Common Project workflows

### Build a "today" dashboard

```javascript JavaScript theme={null}
async function buildTodayDashboard() {
  const todayResponse = await fetch(
    'https://api.vomo.org/v1/projects/today',
    { headers: { Authorization: `Bearer ${token}` } }
  );
  const today = await todayResponse.json();

  return today.data.map((happening) => ({
    id: happening.id,
    timeWindow: `${formatTime(happening.starts_at)}–${formatTime(happening.ends_at)}`,
    participantCount: happening.participant_count,
  }));
}
```

Useful for displaying "what's happening today" in a partner-built dashboard.

### Pull a Project's full schedule

```javascript JavaScript theme={null}
async function getProjectSchedule(projectId) {
  const project = await getProjectDetail(projectId);
  return project?.all_dates ?? [];
}
```

The `all_dates` field on `ProjectDetailResource` provides the full schedule without separate Project Date fetches.

### Find Projects with capacity

```javascript JavaScript theme={null}
async function findProjectsWithCapacity(maxParticipants = 50) {
  const upcoming = await upcomingProjects();
  const withDetail = await Promise.all(
    upcoming.map((p) => getProjectDetail(p.id))
  );

  return withDetail.filter((p) => {
    const next = p?.next_date;
    return next && next.participant_count < maxParticipants;
  });
}
```

For partner-built "find a project to volunteer for" interfaces — surface only Projects with available spots.

### Sync Projects to an external system

```javascript JavaScript theme={null}
async function syncProjectsToExternal(customerId) {
  const lastSync = await getCheckpoint(customerId);

  const params = new URLSearchParams({
    updated_after: lastSync.toISOString(),
  });

  let url = `https://api.vomo.org/v1/projects?${params}`;
  while (url) {
    const page = await fetchPage(url);
    for (const project of page.data) {
      await externalSystem.upsertProject({
        externalId: `vomo-project-${project.id}`,
        name: project.name,
        organizationId: project.organization_id,
        updatedAt: new Date(project.updated_at),
      });
    }
    url = page.links.next;
  }

  await advanceCheckpoint(customerId);
}
```

The `updated_after` filter combined with `links.next` pagination produces a clean incremental sync.

***

## What can't be done via the API

| Capability                               | Status                                     |
| ---------------------------------------- | ------------------------------------------ |
| Create a Project Date / Happening        | Not exposed — schedule managed in admin UI |
| Modify a Project Date's time             | Not exposed                                |
| Cancel a Project Date                    | Not exposed                                |
| Record a participation (sign someone up) | Not exposed                                |
| Check a volunteer in or out              | Not exposed                                |
| Modify participation hours               | Not exposed                                |

Most of these are intentional — they preserve the customer's organizer-controlled scheduling and check-in workflow. Partner integrations that need to push participation data into VOMO should coordinate with VOMO's admin team for alternative paths.

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

***

## A reference Project client

```javascript JavaScript theme={null}
class VomoProjects {
  constructor({ token }) {
    this.token = token;
    this.baseUrl = 'https://api.vomo.org/v1';
  }

  async list(filters = {}) {
    const params = new URLSearchParams(filters);
    const projects = [];
    let url = `${this.baseUrl}/projects?${params}`;

    while (url) {
      const response = await this._fetch(url);
      const page = await response.json();
      projects.push(...page.data.map(this._parseProject));
      url = page.links.next;
    }
    return projects;
  }

  async getById(projectId) {
    const response = await this._fetch(`${this.baseUrl}/projects/${projectId}`);
    if (response.status === 404) return null;
    if (!response.ok) throw new Error(`Failed: ${response.status}`);
    const result = await response.json();
    return this._parseProjectDetail(result.data);
  }

  async getToday() {
    const response = await this._fetch(`${this.baseUrl}/projects/today`);
    const result = await response.json();
    return result.data.map(this._parseHappening);
  }

  async getDateById(projectDateId) {
    const response = await this._fetch(
      `${this.baseUrl}/projects/date/${projectDateId}`
    );
    if (response.status === 404) return null;
    if (!response.ok) throw new Error(`Failed: ${response.status}`);
    return response.json();
  }

  async create(projectData) {
    const response = await this._fetch(`${this.baseUrl}/projects`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(projectData),
    });
    if (!response.ok) throw new Error(`Create failed: ${response.status}`);
    return response.json();
  }

  async update(projectId, projectData) {
    const response = await this._fetch(`${this.baseUrl}/projects/${projectId}`, {
      method: 'PUT',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(projectData),
    });
    if (!response.ok) throw new Error(`Update failed: ${response.status}`);
    return response.json();
  }

  _fetch(url, options = {}) {
    return fetch(url, {
      ...options,
      headers: {
        Authorization: `Bearer ${this.token}`,
        Accept: 'application/json',
        ...options.headers,
      },
    });
  }

  _parseProject(raw) {
    return {
      id: raw.id,
      name: raw.name, // Prefer name over project_name (audit #42)
      description: raw.description,
      url: raw.url,
      organization: raw.organization,
      organizationId: raw.organization_id,
      organizationSlug: raw.organization_slug,
      createdAt: new Date(raw.created_at),
      updatedAt: new Date(raw.updated_at),
      publishedAt: raw.published_at ? new Date(raw.published_at) : null,
      address: raw.address, // May be array or object — see audit #43
      campaigns: raw.campaigns ?? [],
      owners: raw.owners ?? [],
    };
  }

  _parseProjectDetail(raw) {
    return {
      ...this._parseProject(raw),
      pointPerson: raw.point_person ?? null,
      details: raw.details ?? '',
      allowGuests: raw.allow_guests ?? false,
      ageLimit: raw.age_limit ?? null,
      participantApprovalRequired: raw.participant_approval_required ?? false,
      backgroundCheckRequired: raw.background_check_required ?? false,
      draft: raw.draft ?? false,
      privacy: raw.privacy ?? 'PUBLIC',
      allDates: raw.all_dates ?? [],
      nextDate: raw.next_date ?? null,
    };
  }

  _parseHappening(raw) {
    return {
      id: raw.id,
      startsAt: new Date(raw.starts_at),
      endsAt: new Date(raw.ends_at),
      participantCount: raw.participant_count ?? 0,
    };
  }
}
```

***

## Where to go next

<CardGroup cols={2}>
  <Card title="Groups" icon="users" href="/volunteer/concepts/groups">
    The User-organizing resource — Groups and Group Members.
  </Card>

  <Card title="Users" icon="user" href="/volunteer/concepts/users">
    The User resource — including the participations embedded in user details.
  </Card>

  <Card title="The Volunteer Data Model" icon="diagram-project" href="/volunteer/concepts/data-model">
    The full data model context.
  </Card>

  <Card title="Create or Update a Project" icon="briefcase" href="/volunteer/workflows/create-or-update-a-project">
    The workflow walkthrough for Project writes.
  </Card>
</CardGroup>
