Skip to main content
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:
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.
AspectProjectProject Date
PersistenceLong-lived (months or years)Short-lived (a few hours typically)
IdentityThe opportunity (“Saturday Food Bank Shift”)The occurrence (“March 15, 2025 shift”)
Has participants?No — participants are on datesYes — the embedded participants are on the date
Created byCustomer’s organizerSchedule generates them from the Project
API write surfacePOST /projects, PUT /projects/{id}None — managed in admin UI

The Project endpoints

EndpointMethodWhat it does
/projectsGETList Projects (paginated, filterable)
/projectsPOSTCreate a Project
/projects/{id}GETGet a Project’s full detail
/projects/{id}PUTUpdate a Project (full replacement)
/projects/todayGETList Project Dates happening today
/projects/date/{id}GETGet 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.
⚠️ 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.

The Project resource

List shape (ProjectResource)

GET /projects returns an array of ProjectResource objects — the metadata describing the volunteer opportunity:
FieldTypeDescription
typestring"project" — VOMO object type
idintegerThe Project’s stable ID
namestringThe Project’s display name
project_namestringDuplicate of name — see warning below
descriptionstringThe Project’s description
urlstringA URL to the Project’s page in the VOMO UI
organizationstringThe Organization name
organization_idintegerThe Organization ID
organization_slugstringThe Organization’s slug (URL-safe identifier)
updated_atstringISO 8601 datetime
created_atstringISO 8601 datetime
published_atstringWhen the Project was published (or null if draft)
imagesarrayProject images
campaignsarrayCampaigns this Project is attached to
ownersarrayProject organizers/owners
addressobjectThe Project’s location
certificatesarrayCertificates required for this Project
form_completionsarrayForms attached to this Project
⚠️ 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.
⚠️ 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.

Detail shape (ProjectDetailResource)

GET /projects/{id} returns a ProjectDetailResource — a superset of ProjectResource plus the operational policy fields:
FieldTypeDescription
point_personobjectContact for the Project day
detailsstringOperational notes (what to wear, what to bring)
allow_guestsbooleanCan volunteers bring guests?
age_limitintegerMinimum volunteer age
volunteer_questionstringOptional question asked during signup
participant_approval_requiredbooleanDo organizers approve signups?
background_check_requiredbooleanBackground check required to participate?
show_volunteer_counterbooleanDisplay volunteer count publicly?
volunteer_counter_thresholdintegerShow counter only above this count
draftbooleanIs the Project in draft (not yet published)?
privacystring"PUBLIC" or "PRIVATE"
all_datesarrayAll scheduled Project Dates for this Project
next_dateobjectThe 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

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

Available filters

ParameterWhat it filters by
pagePage number (default 1)
activetrue or false — limit to active or inactive Projects
publishedtrue or false — limit to published or draft Projects
anytimetrue or false — include/exclude “anytime” Projects (those without specific dates)
org_slugFilter by Organization slug (comma-separated for multiple)
name_likeSubstring match against Project name
created_beforeProjects created on or before a date
created_afterProjects created on or after a date
updated_beforeProjects updated on or before a date
updated_afterProjects updated on or after a date
dates_beforeProjects with dates that started at or before this datetime
dates_afterProjects 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
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
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
async function projectsForChildOrg(orgSlug) {
  const params = new URLSearchParams({ org_slug: orgSlug });
  return paginate(`https://api.vomo.org/v1/projects?${params}`);
}

Fetching a single project

cURL
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
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

cURL
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
  }'
⚠️ 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.
A successful create returns the new Project. Capture the id for subsequent updates.

Updating a project

cURL
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
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 for caching patterns.

Project Date endpoints

Two endpoints expose Project Dates:
EndpointWhat it returns
GET /projects/todayAll 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:
{
  "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.
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.

GET /projects/date/{id}

Returns a specific Project Date with full detail including the participants:
JavaScript
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();
}
⚠️ 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.

Participants on a Project Date

The participants embedded in a Project Date response use the Participant schema:
FieldTypeDescription
full_namestringThe participant’s full name
display_namestringThe display name (often includes guest count)
guestarrayThe 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
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
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
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
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

CapabilityStatus
Create a Project Date / HappeningNot exposed — schedule managed in admin UI
Modify a Project Date’s timeNot exposed
Cancel a Project DateNot exposed
Record a participation (sign someone up)Not exposed
Check a volunteer in or outNot exposed
Modify participation hoursNot 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.

A reference Project client

JavaScript
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

Groups

The User-organizing resource — Groups and Group Members.

Users

The User resource — including the participations embedded in user details.

The Volunteer Data Model

The full data model context.

Create or Update a Project

The workflow walkthrough for Project writes.
Last modified on May 22, 2026