Skip to main content
A Certificate in the Volunteer API represents a training credential, badge, or achievement that volunteers can 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, CPR certification)
  • Milestone achievements (e.g., 100-hour volunteer recognition)
  • Compliance credentials for regulated programs
Certificates have an expiration period — partner integrations can use this to detect when a volunteer’s credential is about to expire and trigger renewal outreach.

The one endpoint

EndpointMethodWhat it returns
/certificatesGETList Certificates available in the organization
That’s it. The API exposes only Certificate definitions (the “what certificates exist”) — not who has earned which Certificate, or when they earned it. Earned-certificate data isn’t exposed through a dedicated endpoint.
Information about which Users have earned which Certificates may be embedded in the User detail response (GET /users/{id}) and the Project detail response (which lists required certificates). For workflows that need to know who has which Certificate, fetch User details and inspect the user’s earned certifications.

The naming inconsistency

The Certificate resource has the most pronounced naming inconsistency of any resource in the Volunteer API:
Where it appearsName used
Endpoint path/certificates
OpenAPI tagCertifications
operationIdlistCertifications
Schema nameCertificateResource
Schema description"List of Certifications"
⚠️ Spec gap (audit #14): The resource is referred to as “Certificate”, “Certification”, and “Certifications” in different places. The path is /certificates (the canonical URL); the schema is CertificateResource (the canonical type name).For partner integrations, use the path (/certificates) and the schema name (CertificateResource) as your reference points. The variations will be reconciled in a future spec revision.
For the rest of this page, the resource is referred to as Certificate — matching the path and schema name.

The Certificate resource

The CertificateResource schema documents these fields:
FieldTypeDescription
typestring"certificate" — the VOMO object type
idintegerThe Certificate’s stable ID
namestringThe Certificate’s display name
requirementstringDescription of what’s required to earn the Certificate
slugstringA slug for the Certificate (see warning below)
expiration_in_monthsintegerHow long the Certificate remains valid after issuance
creator_idintegerThe User ID of the Certificate’s creator
organization_idintegerThe Organization the Certificate belongs to
created_atstringISO 8601 datetime
updated_atstringISO 8601 datetime
⚠️ Spec gap (audit #17): The CertificateResource schema is incorrectly typed as array in the spec — the same issue affecting FormResource and FormFieldResource. The actual response is a single Certificate object per item in the response’s data array. Code generated from the spec may need manual adjustment.
⚠️ Spec gap (audit #15): The slug field has the same enum-vs-example contradiction documented on FormResource. The enum lists form-field-like values (SHORTTEXT, LONGTEXT, etc.) and the example is a UUID. Treat the slug as a UUID string per the example; the misleading enum will be removed in a future spec revision.
⚠️ Spec gap (audit #16): The id field has minimum: 3, maximum: 45 constraints — string-length-style constraints applied to an integer. Treat IDs as unconstrained integers.

Expiration semantics

The expiration_in_months field captures how long a Certificate remains valid after a User earns it:
ValueMeaning
12Valid for 12 months from issuance
24Valid for 24 months
0 or nullLikely “never expires” (confirm against live data)
For partner integrations, this is the dimension that drives renewal workflows — knowing the validity window for each Certificate type lets the integration compute “when does this user’s certification expire?” given their earn date. The expiration is on the Certificate definition, not on the per-user earned record. So the same Certificate has the same validity window for everyone who earns it.

Listing certificates

cURL
curl https://api.vomo.org/v1/certificates \
  -H "Authorization: Bearer $VOMO_API_TOKEN"
Returns all Certificates in the customer’s organization, paginated:
{
  "data": [
    {
      "type": "certificate",
      "id": 12,
      "name": "Food Handler Certification",
      "requirement": "Complete the food safety training module",
      "slug": "abc-def-123",
      "expiration_in_months": 12,
      "creator_id": 1,
      "organization_id": 100,
      "created_at": "2024-08-01T09:00:00Z",
      "updated_at": "2024-08-01T09:00:00Z"
    }
    /* ... more Certificates ... */
  ],
  "links": { /* ... */ },
  "meta": { /* ... */ }
}
The /certificates endpoint doesn’t document explicit query filters in the spec. Pagination via ?page=N likely works the same as other list endpoints — follow links.next.
JavaScript
async function listAllCertificates() {
  const certs = [];
  let url = 'https://api.vomo.org/v1/certificates';

  while (url) {
    const response = await fetch(url, {
      headers: { Authorization: `Bearer ${token}` },
    });
    if (!response.ok) throw new Error(`Failed: ${response.status}`);

    const page = await response.json();
    certs.push(...page.data);
    url = page.links.next;
  }

  return certs;
}
For most customers, the list of available Certificates is short (typically a handful to a few dozen) — fitting comfortably in memory and changing infrequently. Cache the result aggressively.

Common workflows

Cache and look up Certificates by ID

The most common pattern — read the Certificate list once, cache it, then look up by ID when needed:
JavaScript
class CertificateCache {
  constructor({ token, ttlMs = 60 * 60 * 1000 }) {
    this.token = token;
    this.ttlMs = ttlMs;
    this.cached = null;
    this.cachedAt = 0;
  }

  async getAll() {
    if (this.cached && Date.now() - this.cachedAt < this.ttlMs) {
      return this.cached;
    }

    this.cached = await listAllCertificates();
    this.cachedAt = Date.now();
    return this.cached;
  }

  async getById(id) {
    const all = await this.getAll();
    return all.find((c) => c.id === id) ?? null;
  }

  async getByName(name) {
    const all = await this.getAll();
    return all.find((c) => c.name.toLowerCase() === name.toLowerCase()) ?? null;
  }
}
A 1-hour TTL is reasonable — Certificates change rarely.

Map a Project’s required Certificates

Projects can require Certificates as a prerequisite for volunteering. The Project’s certificates array (on ProjectResource) lists which:
{
  "id": 789,
  "name": "Saturday Food Bank Shift",
  "certificates": [
    { "id": 12, "name": "Food Handler Certification" },
    { "id": 13, "name": "Background Check" }
  ]
}
For partner integrations displaying Project requirements:
JavaScript
async function displayProjectRequirements(projectId) {
  const project = await getProjectDetail(projectId);
  const certCache = new CertificateCache({ token });

  const requirements = [];
  for (const certRef of project.certificates ?? []) {
    const cert = await certCache.getById(certRef.id);
    if (cert) {
      requirements.push({
        name: cert.name,
        description: cert.requirement,
        validForMonths: cert.expiration_in_months,
      });
    }
  }

  return requirements;
}
The result is a list of “what does a volunteer need to qualify for this Project?” — useful for signup pages, eligibility screens, or admin reports.

Expiration renewal outreach

For workflows that watch for Certificates approaching expiration:
JavaScript
async function findUsersWithExpiringCertificates(daysUntilExpiration = 30) {
  // Note: This pattern requires the User detail to expose earned certificates with dates.
  // The exact format isn't documented in the spec — confirm against live data.

  const allUsers = await paginate('https://api.vomo.org/v1/users');
  const certCache = new CertificateCache({ token });
  const expiringPerUser = [];

  for (const userSummary of allUsers) {
    const user = await getUserDetail(userSummary.id);
    const earnedCerts = user.earned_certificates ?? []; // confirm field name from live API

    for (const earned of earnedCerts) {
      const cert = await certCache.getById(earned.certificate_id);
      if (!cert?.expiration_in_months) continue;

      const earnedDate = new Date(earned.earned_at);
      const expirationDate = new Date(earnedDate);
      expirationDate.setMonth(expirationDate.getMonth() + cert.expiration_in_months);

      const daysUntilExp = (expirationDate - Date.now()) / (24 * 60 * 60 * 1000);

      if (daysUntilExp <= daysUntilExpiration && daysUntilExp > 0) {
        expiringPerUser.push({
          user,
          certificate: cert,
          expirationDate,
          daysUntilExpiration: Math.round(daysUntilExp),
        });
      }
    }
  }

  return expiringPerUser;
}
The exact field name and shape of earned-certificate data on the User Detail response (GET /users/{id}) isn’t documented in the OpenAPI spec — the example UserDetailResource only formally documents participations and profile_field_values. The pattern above assumes a field like earned_certificates with certificate_id and earned_at per entry, but confirm against the live API before relying on it.
The output is a list of “User X’s Certificate Y expires in N days” entries — feed this into the customer’s renewal outreach pipeline.

Sync Certificate list to a compliance system

For organizations with regulatory compliance requirements that need Certificate data in an external system:
JavaScript
async function syncCertificatesToComplianceSystem(customerId) {
  const lastSync = await getCheckpoint(customerId, 'certificate_sync');
  const certs = await listAllCertificates();

  for (const cert of certs) {
    const updatedAt = new Date(cert.updated_at);
    if (updatedAt > lastSync) {
      await complianceSystem.upsertCertification({
        externalId: `vomo-cert-${cert.id}`,
        name: cert.name,
        description: cert.requirement,
        validityMonths: cert.expiration_in_months,
        lastUpdated: updatedAt,
      });
    }
  }

  await advanceCheckpoint(customerId, 'certificate_sync', new Date());
}
Run daily or weekly. Certificates change infrequently; a less aggressive cadence is fine.

What can’t be done via the API

CapabilityStatus
Create a CertificateNot exposed — admin UI only
Update a Certificate’s requirement or expirationNot exposed
Delete a CertificateNot exposed
Award a Certificate to a UserNot exposed
Revoke a Certificate from a UserNot exposed
Verify a Certificate’s status for a specific UserNo dedicated endpoint — inspect User detail
If a partner integration needs to programmatically award Certificates (e.g., after a User completes external training), coordinate with VOMO’s admin team for an alternative path. See Understand Write Limitations.

A reference Certificates client

JavaScript
class VomoCertificates {
  constructor({ token, ttlMs = 60 * 60 * 1000 }) {
    this.token = token;
    this.baseUrl = 'https://api.vomo.org/v1';
    this.ttlMs = ttlMs;
    this.cache = null;
    this.cachedAt = 0;
  }

  async list() {
    const certs = [];
    let url = `${this.baseUrl}/certificates`;

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

  async getCached() {
    if (this.cache && Date.now() - this.cachedAt < this.ttlMs) {
      return this.cache;
    }
    this.cache = await this.list();
    this.cachedAt = Date.now();
    return this.cache;
  }

  async getById(certificateId) {
    const all = await this.getCached();
    return all.find((c) => c.id === certificateId) ?? null;
  }

  async getByName(name) {
    const all = await this.getCached();
    return all.find((c) => c.name.toLowerCase() === name.toLowerCase()) ?? null;
  }

  computeExpiration(certificate, earnedDate) {
    if (!certificate.expirationInMonths) return null; // No expiration
    const expirationDate = new Date(earnedDate);
    expirationDate.setMonth(expirationDate.getMonth() + certificate.expirationInMonths);
    return expirationDate;
  }

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

  _parseCertificate(raw) {
    return {
      id: raw.id,
      name: raw.name,
      requirement: raw.requirement ?? '',
      slug: raw.slug, // Treat as UUID per example, not the misleading enum
      expirationInMonths: raw.expiration_in_months ?? null,
      creatorId: raw.creator_id,
      organizationId: raw.organization_id,
      createdAt: new Date(raw.created_at),
      updatedAt: new Date(raw.updated_at),
    };
  }
}
The computeExpiration helper handles the most common operation — given an earned date and a Certificate, compute the expiration date.

Where to go next

Users

Users earn Certificates; the User detail response is where earned-certificate data lives.

Projects and Project Dates

Projects can require Certificates as prerequisites.

Forms and Form Completions

The other read-only structured-data resource family.

The Volunteer Data Model

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