Skip to main content
Forms in the Volunteer API are data-collection templates — waivers, volunteer applications, signup questions, post-event surveys. When a volunteer fills out a Form, the result is a Form Completion containing the volunteer’s responses to each field. The chain is:
Form (template)
  ├── Form Fields (questions)
  │     └── Form Field Options (dropdown values)
  └── Form Completion (volunteer's submission)
        └── Form Field Responses (per-field answers)
This page covers the five-resource Form family, the three read endpoints, and the practical patterns for working with form data — including the audit-flagged quirks in the spec.

The three endpoints

EndpointMethodWhat it returns
/formsGETList Forms (template-level)
/forms/{id}/completionsGETList all completions of a specific Form
/forms/{id}/completions/{completion}GETGet a single Form Completion with field responses
All Form endpoints are read-only. Forms are created and managed in the VOMO admin UI; volunteers submit Form Completions through the VOMO UI; partner integrations consume the data.
⚠️ Spec gap (audit #20): The path parameter for the third endpoint is {completion} rather than the more conventional {completionId}. The parameter descriptions in the spec also contain typos (“VOMO From ID” instead of “VOMO Form ID” — audit #19). The path works as documented; the names will be cleaned up in a future spec revision.

The Form family of resources

The Form resource family has five distinct schemas, each capturing a different layer:

FormResource — the template

The top-level Form record describing the data-collection template:
FieldTypeDescription
typestring"form" — the VOMO object type
idintegerThe Form’s stable ID
namestringThe Form’s display name
descriptionstringA description of the Form’s purpose
form_typestringThe Form type — see Form types below
slugstringA slug for the Form — see warning below
author_idintegerThe User ID of the Form’s creator
organization_idintegerThe Organization the Form belongs to
fieldsarrayThe Form’s fields (see FormFieldResource)
created_atstringISO 8601 datetime
updated_atstringISO 8601 datetime
deleted_atstring or nullWhen archived (or null if active)
⚠️ Spec gap (audit #17): The FormResource schema is incorrectly typed as array in the spec. The actual resource is a single object — the field set above is on the items inside that erroneous array. Code generated from the spec may need manual adjustment.
⚠️ Spec gap (audit #15): The slug field is documented with two contradictory pieces of information:
  • An enum: ['SHORTTEXT', 'LONGTEXT', 'DROPDOWN', 'MULTIPLESELECT', 'WAIVER', 'TEXTSINGLE'] (these look like form-field types)
  • An example: "2d8614a5-31ed-4810-8da2-448f59463e43" (a UUID)
The enum and example can’t both be right. The example suggests the slug is a UUID identifier for the Form; the enum looks like it was copy-pasted from a Form Field’s field_type. Treat the slug as a UUID string per the example. The enum will be removed in a future spec revision.
⚠️ Spec gap (audit #16): The id field has minimum: 3, maximum: 45 constraints — these are string-length-style constraints applied to an integer ID. The actual ID range almost certainly isn’t 3 to 45; treat IDs as unconstrained integers.

FormFieldResource — the questions

Each Form has an array of Fields — the individual questions the volunteer answers:
FieldTypeDescription
typestring"form_field" — VOMO object type
idintegerThe Field’s stable ID
namestringThe Field’s display name (the question text)
descriptionstringAdditional description / instructions
field_typestringThe Field’s data type (see Field types)
slugstringA slug for the Field
show_in_profilebooleanWhether to display this Field’s value on the user’s profile
optionsarrayFor dropdowns and multi-selects, the available options (FormFieldOptionResource)
⚠️ Spec gap (audit #17): FormFieldResource is also typed as array in the spec — same issue as FormResource.

FormFieldOptionResource — dropdown choices

For Fields with field_type of DROPDOWN or MULTIPLESELECT, the available options:
FieldTypeDescription
idintegerThe Option’s stable ID
namestringThe display label
valuestringThe stored value
enabledbooleanWhether the Option is currently selectable
weightintegerDisplay order
The weight field controls display order. Lower weights appear earlier in the list.

FormCompletionResource — a volunteer’s submission

When a volunteer completes a Form, a Form Completion record is created:
FieldTypeDescription
typestring"form_completion" — VOMO object type
idintegerThe Completion’s stable ID
form_idintegerThe Form that was completed
user_idintegerThe User who submitted
project_idintegerThe Project this completion is associated with (may be null)
field_responsesarrayThe User’s responses (FormFieldResponseResource[])
created_atstringWhen the completion was submitted
updated_atstringWhen the completion was last modified
This is the central “what did this volunteer say in this form” record. The field_responses array captures the actual answers.

FormFieldResponseResource — a single answer

Each entry in field_responses represents one Field’s answer:
FieldTypeDescription
typestring"form_field_response" — VOMO object type
idintegerThe Response’s stable ID
form_completion_idintegerThe Completion this Response belongs to
user_idintegerThe User who provided the answer
field_idintegerThe Form Field the answer is for
valuestringThe User’s answer
Even numeric or boolean Field values come back as strings. Parse them based on the Field’s field_type when displaying or processing.

Form types

The form_type enum determines what context the Form is used in:
ValueMeaning
PROJECTA Form attached to a Project (waivers, project-specific signups, post-event surveys)
⚠️ Spec gap (audit #54): The spec documents only PROJECT as a valid form_type value. Other types likely exist in production (organization-wide signups, user-profile forms, etc.) but aren’t documented. Treat unknown values gracefully — log them but don’t crash.

Field types

The field_type on a Form Field determines what input the volunteer provides. Likely values (from the Form slug enum, which appears to have been miscategorized — see audit #15):
field_type valueWhat it represents
SHORTTEXTA single-line text input
LONGTEXTA multi-line text input
DROPDOWNA dropdown menu — values come from options[]
MULTIPLESELECTA multi-select — values come from options[]
WAIVERA waiver acceptance (typically a checkbox + legal text)
TEXTSINGLE(Likely equivalent to SHORTTEXT)
For partner integrations parsing Form completions:
JavaScript
function parseFieldValue(field, response) {
  switch (field.field_type) {
    case 'SHORTTEXT':
    case 'LONGTEXT':
    case 'TEXTSINGLE':
      return response.value; // String as-is
    case 'WAIVER':
      return response.value === 'true' || response.value === '1';
    case 'DROPDOWN':
      // Look up the option by value
      return field.options?.find((o) => o.value === response.value)?.name ?? response.value;
    case 'MULTIPLESELECT':
      // Comma-separated values; look up each
      const values = response.value.split(',').map((v) => v.trim());
      return values.map((v) =>
        field.options?.find((o) => o.value === v)?.name ?? v
      );
    default:
      return response.value; // Unknown type — return raw string
  }
}
The exact storage format for MULTIPLESELECT (comma-separated vs. JSON array vs. something else) isn’t documented in the spec; the pattern above handles the most likely formats defensively.

Listing forms

cURL
curl "https://api.vomo.org/v1/forms?name_like=waiver&include_archived=false" \
  -H "Authorization: Bearer $VOMO_API_TOKEN"

Available filters

ParameterWhat it filters by
name_likeSubstring match against Form name
created_beforeForms created on or before a date
created_afterForms created on or after a date
updated_beforeForms updated on or before a date
updated_afterForms updated on or after a date
include_archivedInclude archived (soft-deleted) Forms
⚠️ Spec gap (audit #18): The include_archived parameter is typed string in the spec but is functionally a boolean — accepts "true" or "false". Code should send the boolean as a string ("true" / "false").

Common list patterns

Active Forms only:
JavaScript
async function activeForms() {
  return paginate('https://api.vomo.org/v1/forms?include_archived=false');
}
Find Forms by name:
JavaScript
async function findFormsLike(searchTerm) {
  const params = new URLSearchParams({ name_like: searchTerm });
  return paginate(`https://api.vomo.org/v1/forms?${params}`);
}
Recently-updated Forms (for change detection):
JavaScript
async function recentlyUpdatedForms(sinceDate) {
  const params = new URLSearchParams({
    updated_after: sinceDate.toISOString(),
  });
  return paginate(`https://api.vomo.org/v1/forms?${params}`);
}
Useful for polling integrations that watch for Form structure changes.

Reading Form completions

cURL
curl "https://api.vomo.org/v1/forms/789/completions" \
  -H "Authorization: Bearer $VOMO_API_TOKEN"
Returns all completions of Form #789, paginated.

Available filters

ParameterWhat it filters by
user_idOnly completions by a specific User
created_beforeCompletions created on or before a date
created_afterCompletions created on or after a date
updated_beforeCompletions updated on or before a date
updated_afterCompletions updated on or after a date

Common list patterns

Completions for a specific user across one form:
JavaScript
async function userCompletionsForForm(formId, userId) {
  const params = new URLSearchParams({ user_id: userId.toString() });
  return paginate(`https://api.vomo.org/v1/forms/${formId}/completions?${params}`);
}
All completions since the last sync:
JavaScript
async function newCompletionsForForm(formId, since) {
  const params = new URLSearchParams({
    created_after: since.toISOString(),
  });
  return paginate(`https://api.vomo.org/v1/forms/${formId}/completions?${params}`);
}
A polling integration pulling form completions on a schedule would iterate over active Forms, then for each Form pull new completions since the checkpoint.

Fetching a single completion

cURL
curl https://api.vomo.org/v1/forms/789/completions/12345 \
  -H "Authorization: Bearer $VOMO_API_TOKEN"
Returns a single FormCompletionResource with full field_responses[]:
JavaScript
async function getFormCompletion(formId, completionId) {
  const response = await fetch(
    `https://api.vomo.org/v1/forms/${formId}/completions/${completionId}`,
    { 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 result.data;
}
The list endpoint already returns completions with their field responses, so the single-completion endpoint is most useful for re-fetching a specific completion when you have the ID but not the cached data.

Reading field responses with the Field definitions

A common pattern: display a Form completion alongside the Field definitions so the responses make sense in context.
JavaScript
async function getCompletionWithFieldContext(formId, completionId) {
  // 1. Fetch the Form (to get Field definitions)
  const forms = await fetch(`https://api.vomo.org/v1/forms?name_like=`, {
    headers: { Authorization: `Bearer ${token}` },
  });
  // ... find the specific form
  const form = (await forms.json()).data.find((f) => f.id === formId);

  // 2. Fetch the Completion
  const completion = await getFormCompletion(formId, completionId);
  if (!completion) return null;

  // 3. Join responses with field definitions
  const fieldsById = new Map(form.fields.map((f) => [f.id, f]));

  return {
    completionId: completion.id,
    user_id: completion.user_id,
    project_id: completion.project_id,
    submittedAt: new Date(completion.created_at),
    fields: completion.field_responses.map((r) => {
      const field = fieldsById.get(r.field_id);
      return {
        question: field?.name ?? `Field ${r.field_id}`,
        type: field?.field_type ?? 'UNKNOWN',
        value: parseFieldValue(field, r),
      };
    }),
  };
}
The result is a displayable Form Completion with question text, field type, and parsed value for each response — what you’d show in a UI showing “Bruce Wayne’s volunteer application.” For partner integrations doing this frequently, cache the Form definitions — they change rarely and the lookup happens often.

Common workflows

Sync Form completions to an external system

For partner integrations syncing volunteer applications, waiver acceptances, or survey responses into an external CRM:
JavaScript
async function syncFormCompletions(customerId, formId) {
  const lastSync = await getCheckpoint(customerId, `form_${formId}`);

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

  let url = `https://api.vomo.org/v1/forms/${formId}/completions?${params}`;
  while (url) {
    const page = await fetchPage(url);
    for (const completion of page.data) {
      await externalSystem.recordFormCompletion({
        externalId: `vomo-form-${completion.id}`,
        formId: completion.form_id,
        userId: completion.user_id,
        projectId: completion.project_id,
        submittedAt: new Date(completion.created_at),
        responses: completion.field_responses,
      });
    }
    url = page.links.next;
  }

  await advanceCheckpoint(customerId, `form_${formId}`, new Date());
}
Run this on a schedule per Form the customer wants synced.

Build a per-user form history

For showing a single user’s full form submission history:
JavaScript
async function userFormHistory(userId) {
  // 1. Get all Forms (to know what's available)
  const forms = await activeForms();

  // 2. For each Form, get this user's completions
  const allCompletions = [];
  for (const form of forms) {
    const params = new URLSearchParams({ user_id: userId.toString() });
    const url = `https://api.vomo.org/v1/forms/${form.id}/completions?${params}`;
    const response = await fetch(url, { headers: { Authorization: `Bearer ${token}` } });
    const result = await response.json();

    for (const completion of result.data) {
      allCompletions.push({
        formName: form.name,
        formId: form.id,
        completion,
      });
    }
  }

  return allCompletions.sort((a, b) =>
    new Date(b.completion.created_at) - new Date(a.completion.created_at)
  );
}
This is N+1 (one Form list + one Completion list per Form). For accounts with many Forms but where most users have few completions, this is acceptable; for high-volume accounts, consider caching.

Detect waiver expiration

For organizations requiring annual waiver renewal:
JavaScript
async function findUsersNeedingWaiver(waiverFormId) {
  const oneYearAgo = new Date();
  oneYearAgo.setFullYear(oneYearAgo.getFullYear() - 1);

  // 1. Get all completions of the waiver
  const allCompletions = await paginate(
    `https://api.vomo.org/v1/forms/${waiverFormId}/completions`
  );

  // 2. Find the most recent completion per user
  const latestByUser = new Map();
  for (const c of allCompletions) {
    const existing = latestByUser.get(c.user_id);
    if (!existing || new Date(c.created_at) > new Date(existing.created_at)) {
      latestByUser.set(c.user_id, c);
    }
  }

  // 3. Filter to users whose latest is older than a year
  return Array.from(latestByUser.entries())
    .filter(([_, c]) => new Date(c.created_at) < oneYearAgo)
    .map(([userId, c]) => ({ userId, lastCompletedAt: c.created_at }));
}
The result is a list of users whose waiver completion has aged out — candidates for renewal outreach.

Aggregate Form responses for reporting

For survey or feedback Forms:
JavaScript
async function aggregateFormResponses(formId) {
  const form = await getFormDetail(formId);
  const completions = await paginate(`https://api.vomo.org/v1/forms/${formId}/completions`);

  const aggregateByField = new Map();

  for (const completion of completions) {
    for (const response of completion.field_responses) {
      if (!aggregateByField.has(response.field_id)) {
        aggregateByField.set(response.field_id, {
          field: form.fields.find((f) => f.id === response.field_id),
          values: [],
        });
      }
      aggregateByField.get(response.field_id).values.push(response.value);
    }
  }

  // Format the aggregates per field type
  return Array.from(aggregateByField.entries()).map(([fieldId, data]) => ({
    fieldId,
    question: data.field?.name,
    type: data.field?.field_type,
    distribution: tallyDistribution(data.values, data.field),
  }));
}
Useful for survey-style reports: “12 volunteers said yes, 3 said no, 5 left it blank.”

What can’t be done via the API

CapabilityStatus
Create a FormNot exposed — admin UI only
Update a Form’s structure (add/remove fields)Not exposed
Delete or archive a FormNot exposed
Submit a Form Completion on behalf of a userNot exposed
Update an existing Completion’s responsesNot exposed
Delete a Form CompletionNot exposed
Configure Field options programmaticallyNot exposed
If a partner integration needs to push Form data into VOMO (e.g., importing waivers signed externally), coordinate with VOMO’s admin team for an alternative path — typically a CSV import. See Understand Write Limitations.

A reference Forms client

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

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

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

  async getCompletions(formId, filters = {}) {
    const params = new URLSearchParams(filters);
    const completions = [];
    let url = `${this.baseUrl}/forms/${formId}/completions?${params}`;

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

  async getCompletion(formId, completionId) {
    const response = await this._fetch(
      `${this.baseUrl}/forms/${formId}/completions/${completionId}`
    );
    if (response.status === 404) return null;
    if (!response.ok) throw new Error(`Failed: ${response.status}`);
    const result = await response.json();
    return this._parseCompletion(result.data);
  }

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

  _parseForm(raw) {
    return {
      id: raw.id,
      name: raw.name,
      description: raw.description ?? '',
      formType: raw.form_type,
      slug: raw.slug, // Treat as UUID per the example, not the misleading enum
      authorId: raw.author_id,
      organizationId: raw.organization_id,
      fields: (raw.fields ?? []).map(this._parseField),
      createdAt: new Date(raw.created_at),
      updatedAt: new Date(raw.updated_at),
      deletedAt: raw.deleted_at ? new Date(raw.deleted_at) : null,
    };
  }

  _parseField(raw) {
    return {
      id: raw.id,
      name: raw.name,
      description: raw.description ?? '',
      fieldType: raw.field_type,
      slug: raw.slug,
      showInProfile: raw.show_in_profile ?? false,
      options: (raw.options ?? []).map(this._parseOption),
    };
  }

  _parseOption(raw) {
    return {
      id: raw.id,
      name: raw.name,
      value: raw.value,
      enabled: raw.enabled ?? true,
      weight: raw.weight ?? 0,
    };
  }

  _parseCompletion(raw) {
    return {
      id: raw.id,
      formId: raw.form_id,
      userId: raw.user_id,
      projectId: raw.project_id ?? null,
      fieldResponses: (raw.field_responses ?? []).map(this._parseFieldResponse),
      createdAt: new Date(raw.created_at),
      updatedAt: new Date(raw.updated_at),
    };
  }

  _parseFieldResponse(raw) {
    return {
      id: raw.id,
      formCompletionId: raw.form_completion_id,
      userId: raw.user_id,
      fieldId: raw.field_id,
      value: raw.value,
    };
  }
}
The parsers handle the defensive cases — missing optional fields, the array-vs-object schema confusion (audit #17 affects the top-level FormResource and FormFieldResource but doesn’t reach parser logic if you treat the response as the array-of-one-record it sometimes appears as).

Where to go next

Certificates

The other read-only resource — training and achievement credentials.

Users

Users complete Forms; the user_id is the linkage to Form Completions.

Projects and Project Dates

Forms are typically attached to Projects.

The Volunteer Data Model

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