The Campaign resource — read-only umbrella initiatives that group multiple Projects, plus the patterns for filtering and attributing volunteer activity by Campaign.
A Campaign in the Volunteer API represents a longer-running initiative that spans multiple Projects. Where Projects represent specific volunteer opportunities (“Saturday Food Bank Shift”), Campaigns represent the broader effort that ties multiple opportunities together (“Summer Outreach 2025”, “Holiday Giving Drive”, “Earth Day Initiative”).For partner integrations, Campaigns are the natural reporting and attribution dimension — “how did the Summer Outreach campaign perform?”, “show me all Projects in the Holiday Drive”. They’re read-only from the API; Campaign creation and management happens in the VOMO admin UI.
There are no write endpoints for Campaigns. The two endpoints are sufficient for the read-oriented integration patterns Campaigns support.
⚠️ Spec gap (audit #4): Both Campaign endpoints have empty schema: {} in the spec — the response shape is only documented through inline examples. The CampaignResource schema component exists but isn’t referenced from either endpoint. Build parsers from actual response shapes.
The CampaignResource schema documents these fields:
Field
Type
Description
type
string
"campaign" — the VOMO object type
id
integer
The Campaign’s stable ID
campaign_name
string
The Campaign’s display name
description
string
The Campaign’s description
url
string
A URL to the Campaign’s page in VOMO
organization
string
The Organization name the Campaign belongs to
organization_slug
string
The Organization’s slug
author_id
integer
The User ID of the person who created the Campaign
logo_url
string
URL for the Campaign logo image
updated_at
string
ISO 8601 datetime
created_at
string
ISO 8601 datetime
⚠️ Spec gap (audit #13):CampaignResource.description reads "A VOMO Project" — a copy-paste error. The schema describes a Campaign, not a Project. The description will be corrected in a future spec revision.
The campaign_name field name is unusual — it includes a redundant resource-type prefix. The audit flagged this (#10) as inconsistent with the standard convention of plain name used in other Virtuous APIs.
API
Resource name field
CRM+
Name (PascalCase)
Raise
name
Volunteer (Project)
Both name AND project_name (duplicate)
Volunteer (Campaign)
Only campaign_name (prefixed)
For partner integrations:
JavaScript
function parseCampaign(raw) { return { id: raw.id, name: raw.campaign_name, // Map to a normalized 'name' field description: raw.description, url: raw.url, organization: raw.organization, organizationSlug: raw.organization_slug, authorId: raw.author_id, logoUrl: raw.logo_url, createdAt: new Date(raw.created_at), updatedAt: new Date(raw.updated_at), };}
Mapping campaign_name to name in your integration’s internal representation produces consistency across CRM+, Raise, and Volunteer Campaign records.
Campaigns belong to specific organizations within the family. The organization and organization_slug fields identify which:
Use case
Field to use
Display the org name in a UI
organization
Filter Campaign queries by org
organization_slug (used with the org_slug query parameter)
Programmatic routing
organization_slug (stable, URL-safe)
There’s no organization_id field documented on Campaigns (unlike Projects, which expose all three: organization, organization_id, organization_slug). Use organization_slug as the identifier for org-based logic.
These mirror the filter set on /projects (minus the Project-specific date filters). The org_slug parameter is the canonical way to filter Campaigns to specific organizations within the family.
async function getCampaign(campaignId) { const response = await fetch( `https://api.vomo.org/v1/campaigns/${campaignId}`, { 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 parseCampaign(result.data);}
⚠️ Spec gap (audit #4):GET /campaigns/{id} has empty schema: {} in the spec. The response likely includes additional fields beyond what’s in CampaignResource (such as an embedded Project list), but the exact extended shape isn’t formally specified.
Campaigns group Projects. The relationship is captured on the Project side — each Project’s ProjectResource includes a campaigns array showing which Campaigns it belongs to:
A Project can belong to multiple Campaigns (an outreach event can be both part of the seasonal “Summer Outreach” Campaign and the issue-focused “Food Security” Campaign).
A common reporting question: “how many people volunteered for this Campaign?”
JavaScript
async function uniqueVolunteersInCampaign(campaignId) { const projects = await projectsInCampaign(campaignId); const userIds = new Set(); for (const project of projects) { const projectDetail = await getProjectDetail(project.id); // Walk all dates of this project; collect unique user IDs from participations for (const projectDate of projectDetail.all_dates ?? []) { const dateDetail = await getProjectDate(projectDate.id); for (const participant of dateDetail.participants ?? []) { userIds.add(participant.user_id); } } } return userIds.size;}
This is expensive (N+1+M — one Projects query, one Project detail per Project, one Project Date detail per Date). For large Campaigns with many Projects and many Project Dates, consider:
Caching the Campaign’s project list (changes infrequently)
Caching Project Date participants by date ID (each date’s participants don’t change after the date passes)
Doing the aggregation offline as a reconciliation job rather than per-request
For partner integrations mirroring Campaigns into an external CRM or reporting tool:
JavaScript
async function syncCampaignsToExternal(customerId) { const lastSync = await getCheckpoint(customerId); const params = new URLSearchParams({ updated_after: lastSync.toISOString(), }); let url = `https://api.vomo.org/v1/campaigns?${params}`; while (url) { const page = await fetchPage(url); for (const campaign of page.data) { await externalSystem.upsertCampaign({ externalId: `vomo-campaign-${campaign.id}`, name: campaign.campaign_name, description: campaign.description, organizationSlug: campaign.organization_slug, updatedAt: new Date(campaign.updated_at), }); } url = page.links.next; } await advanceCheckpoint(customerId);}
The updated_after filter combined with links.next pagination produces a clean incremental sync. Schedule this daily or hourly depending on the customer’s reporting cadence.
Not exposed — Project ↔ Campaign association is admin-UI-managed
Schedule a Campaign with start/end dates
Campaign timing is captured in the admin UI; not API-exposed
If partner integrations need to programmatically create or modify Campaigns, coordinate with VOMO’s admin team. The most common request — creating a per-customer Campaign from an external system — typically becomes an admin-team workflow rather than an API workflow.See Understand Write Limitations.
The campaign_name → name normalization is the key transformation — it produces a Campaign object that looks consistent with how other systems represent Campaign records.