Skip to main content
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.

The two endpoints

EndpointMethodWhat it returns
/campaignsGETList Campaigns (paginated, filterable)
/campaigns/{id}GETGet a single Campaign
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 Campaign resource

The CampaignResource schema documents these fields:
FieldTypeDescription
typestring"campaign" — the VOMO object type
idintegerThe Campaign’s stable ID
campaign_namestringThe Campaign’s display name
descriptionstringThe Campaign’s description
urlstringA URL to the Campaign’s page in VOMO
organizationstringThe Organization name the Campaign belongs to
organization_slugstringThe Organization’s slug
author_idintegerThe User ID of the person who created the Campaign
logo_urlstringURL for the Campaign logo image
updated_atstringISO 8601 datetime
created_atstringISO 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.

About the campaign_name field

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.
APIResource name field
CRM+Name (PascalCase)
Raisename
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.

Organization attribution

Campaigns belong to specific organizations within the family. The organization and organization_slug fields identify which:
Use caseField to use
Display the org name in a UIorganization
Filter Campaign queries by orgorganization_slug (used with the org_slug query parameter)
Programmatic routingorganization_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.

Listing campaigns

cURL
curl "https://api.vomo.org/v1/campaigns?name_like=outreach" \
  -H "Authorization: Bearer $VOMO_API_TOKEN"

Available filters

ParameterWhat it filters by
pagePage number (default 1)
org_slugOne or more org slugs (comma-separated)
name_likeSubstring match against Campaign name
created_beforeCampaigns created on or before a date
created_afterCampaigns created on or after a date
updated_beforeCampaigns updated on or before a date
updated_afterCampaigns updated on or after a date
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.

Common list patterns

Campaigns for a specific organization:
JavaScript
async function campaignsForOrg(orgSlug) {
  const params = new URLSearchParams({ org_slug: orgSlug });
  return paginate(`https://api.vomo.org/v1/campaigns?${params}`);
}
Campaigns for multiple child organizations:
JavaScript
async function campaignsForOrgs(orgSlugs) {
  const params = new URLSearchParams({ org_slug: orgSlugs.join(',') });
  return paginate(`https://api.vomo.org/v1/campaigns?${params}`);
}
Recently-created campaigns:
JavaScript
async function recentCampaigns(sinceDate) {
  const params = new URLSearchParams({
    created_after: sinceDate.toISOString(),
  });
  return paginate(`https://api.vomo.org/v1/campaigns?${params}`);
}
Find a Campaign by partial name (interactive search):
JavaScript
async function findCampaignsLike(searchTerm) {
  const params = new URLSearchParams({ name_like: searchTerm });
  const response = await fetch(
    `https://api.vomo.org/v1/campaigns?${params}`,
    { headers: { Authorization: `Bearer ${token}` } }
  );
  const page = await response.json();
  return page.data;
}

Fetching a single campaign

cURL
curl https://api.vomo.org/v1/campaigns/456 \
  -H "Authorization: Bearer $VOMO_API_TOKEN"
Returns the CampaignResource wrapped in data:
JavaScript
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 and Projects

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:
{
  "type": "project",
  "id": 789,
  "name": "Saturday Food Bank Shift",
  "campaigns": [
    { "id": 456, "campaign_name": "Summer Outreach 2025" },
    { "id": 457, "campaign_name": "Food Security Initiative" }
  ]
}
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).

Finding Projects in a Campaign

Two approaches:
ApproachWhen to use
Query Projects, filter client-side by Campaign IDSimpler; works for any Campaign
Fetch Campaign detail, hope for embedded ProjectsDepends on whether GET /campaigns/{id} returns embedded Projects (spec doesn’t formalize this)
The first approach is more reliable:
JavaScript
async function projectsInCampaign(campaignId) {
  const allProjects = await paginate('https://api.vomo.org/v1/projects');
  return allProjects.filter((p) =>
    (p.campaigns ?? []).some((c) => c.id === campaignId)
  );
}
For partner integrations doing this query frequently, cache the Campaign → Projects mapping rather than re-querying on every request.

Counting volunteers in a 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
See API Performance Tips for the broader pattern.

Common workflows

Campaign performance dashboard

For a “how is Campaign X doing?” dashboard:
JavaScript
async function buildCampaignDashboard(campaignId) {
  const campaign = await getCampaign(campaignId);
  if (!campaign) return null;

  const projects = await projectsInCampaign(campaignId);

  const activeProjects = projects.filter((p) => /* check active status */).length;
  const totalParticipations = await countParticipationsForProjects(projects);
  const uniqueVolunteers = await uniqueVolunteersInCampaign(campaignId);

  return {
    name: campaign.name,
    description: campaign.description,
    url: campaign.url,
    logoUrl: campaign.logoUrl,
    organization: campaign.organization,
    metrics: {
      totalProjects: projects.length,
      activeProjects,
      totalParticipations,
      uniqueVolunteers,
    },
    daysActive: daysSince(campaign.createdAt),
  };
}
Aggregate the Campaign’s Project-level data into Campaign-level metrics. Cache aggressively — these dashboard reads are expensive.

Cross-Campaign comparison

JavaScript
async function compareCampaigns(campaignIds) {
  const results = await Promise.all(
    campaignIds.map(async (id) => {
      const campaign = await getCampaign(id);
      const projects = await projectsInCampaign(id);
      return {
        campaign,
        projectCount: projects.length,
      };
    })
  );

  return results.map((r) => ({
    id: r.campaign.id,
    name: r.campaign.name,
    projectCount: r.projectCount,
  }));
}
Useful for side-by-side comparisons of similar Campaigns (“how did Summer 2024 compare to Summer 2025?”).

Sync Campaign list to an external system

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.

What can’t be done via the API

CapabilityStatus
Create a CampaignNot exposed — admin UI only
Update a Campaign’s metadataNot exposed
Delete a CampaignNot exposed
Add a Project to a CampaignNot exposed — Project ↔ Campaign association is admin-UI-managed
Schedule a Campaign with start/end datesCampaign 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.

A reference Campaigns client

JavaScript
class VomoCampaigns {
  constructor({ token }) {
    this.token = token;
    this.baseUrl = 'https://api.vomo.org/v1';
  }

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

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

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

  async forOrg(orgSlug, additionalFilters = {}) {
    return this.list({ org_slug: orgSlug, ...additionalFilters });
  }

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

  _parseCampaign(raw) {
    return {
      id: raw.id,
      name: raw.campaign_name,  // Normalize to 'name'
      description: raw.description ?? '',
      url: raw.url,
      organization: raw.organization,
      organizationSlug: raw.organization_slug,
      authorId: raw.author_id,
      logoUrl: raw.logo_url ?? null,
      createdAt: new Date(raw.created_at),
      updatedAt: new Date(raw.updated_at),
    };
  }
}
The campaign_namename normalization is the key transformation — it produces a Campaign object that looks consistent with how other systems represent Campaign records.

Where to go next

Projects and Project Dates

The Projects that Campaigns group together.

Organizations and Org Family

Organizations own Campaigns within the family.

Forms and Form Completions

Forms are typically attached to Projects within Campaigns.

The Volunteer Data Model

The full data model context for Campaigns.
Last modified on May 22, 2026