> ## Documentation Index
> Fetch the complete documentation index at: https://docs.virtuous.org/llms.txt
> Use this file to discover all available pages before exploring further.

# Forms & Form Completions

> The Form resource family — Forms, Form Fields, Form Completions, and Field Responses. The chain that captures structured data from volunteers, with the spec-flagged quirks partner integrations need to know.

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:

```text theme={null}
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

| Endpoint                               | Method | What it returns                                   |
| -------------------------------------- | ------ | ------------------------------------------------- |
| `/forms`                               | GET    | List Forms (template-level)                       |
| `/forms/{id}/completions`              | GET    | List all completions of a specific Form           |
| `/forms/{id}/completions/{completion}` | GET    | Get 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.

<Warning>
  **⚠️ 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.
</Warning>

***

## 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:

| Field             | Type           | Description                                         |
| ----------------- | -------------- | --------------------------------------------------- |
| `type`            | string         | `"form"` — the VOMO object type                     |
| `id`              | integer        | The Form's stable ID                                |
| `name`            | string         | The Form's display name                             |
| `description`     | string         | A description of the Form's purpose                 |
| `form_type`       | string         | The Form type — see [Form types](#form-types) below |
| `slug`            | string         | A slug for the Form — see warning below             |
| `author_id`       | integer        | The User ID of the Form's creator                   |
| `organization_id` | integer        | The Organization the Form belongs to                |
| `fields`          | array          | The Form's fields (see `FormFieldResource`)         |
| `created_at`      | string         | ISO 8601 datetime                                   |
| `updated_at`      | string         | ISO 8601 datetime                                   |
| `deleted_at`      | string or null | When archived (or null if active)                   |

<Warning>
  **⚠️ 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.
</Warning>

<Warning>
  **⚠️ 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.
</Warning>

<Warning>
  **⚠️ 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.
</Warning>

### `FormFieldResource` — the questions

Each Form has an array of Fields — the individual questions the volunteer answers:

| Field             | Type    | Description                                                                        |
| ----------------- | ------- | ---------------------------------------------------------------------------------- |
| `type`            | string  | `"form_field"` — VOMO object type                                                  |
| `id`              | integer | The Field's stable ID                                                              |
| `name`            | string  | The Field's display name (the question text)                                       |
| `description`     | string  | Additional description / instructions                                              |
| `field_type`      | string  | The Field's data type (see [Field types](#field-types))                            |
| `slug`            | string  | A slug for the Field                                                               |
| `show_in_profile` | boolean | Whether to display this Field's value on the user's profile                        |
| `options`         | array   | For dropdowns and multi-selects, the available options (`FormFieldOptionResource`) |

<Warning>
  **⚠️ Spec gap (audit #17):** `FormFieldResource` is also typed as `array` in the spec — same issue as `FormResource`.
</Warning>

### `FormFieldOptionResource` — dropdown choices

For Fields with `field_type` of `DROPDOWN` or `MULTIPLESELECT`, the available options:

| Field     | Type    | Description                                |
| --------- | ------- | ------------------------------------------ |
| `id`      | integer | The Option's stable ID                     |
| `name`    | string  | The display label                          |
| `value`   | string  | The stored value                           |
| `enabled` | boolean | Whether the Option is currently selectable |
| `weight`  | integer | Display 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:

| Field             | Type    | Description                                                  |
| ----------------- | ------- | ------------------------------------------------------------ |
| `type`            | string  | `"form_completion"` — VOMO object type                       |
| `id`              | integer | The Completion's stable ID                                   |
| `form_id`         | integer | The Form that was completed                                  |
| `user_id`         | integer | The User who submitted                                       |
| `project_id`      | integer | The Project this completion is associated with (may be null) |
| `field_responses` | array   | The User's responses (`FormFieldResponseResource[]`)         |
| `created_at`      | string  | When the completion was submitted                            |
| `updated_at`      | string  | When 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:

| Field                | Type    | Description                                |
| -------------------- | ------- | ------------------------------------------ |
| `type`               | string  | `"form_field_response"` — VOMO object type |
| `id`                 | integer | The Response's stable ID                   |
| `form_completion_id` | integer | The Completion this Response belongs to    |
| `user_id`            | integer | The User who provided the answer           |
| `field_id`           | integer | The Form Field the answer is for           |
| `value`              | string  | The 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:

| Value     | Meaning                                                                              |
| --------- | ------------------------------------------------------------------------------------ |
| `PROJECT` | A Form attached to a Project (waivers, project-specific signups, post-event surveys) |

<Warning>
  **⚠️ 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.
</Warning>

***

## 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` value | What it represents                                      |
| ------------------ | ------------------------------------------------------- |
| `SHORTTEXT`        | A single-line text input                                |
| `LONGTEXT`         | A multi-line text input                                 |
| `DROPDOWN`         | A dropdown menu — values come from `options[]`          |
| `MULTIPLESELECT`   | A multi-select — values come from `options[]`           |
| `WAIVER`           | A waiver acceptance (typically a checkbox + legal text) |
| `TEXTSINGLE`       | (Likely equivalent to `SHORTTEXT`)                      |

For partner integrations parsing Form completions:

```javascript JavaScript theme={null}
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

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

### Available filters

| Parameter          | What it filters by                    |
| ------------------ | ------------------------------------- |
| `name_like`        | Substring match against Form name     |
| `created_before`   | Forms created on or before a date     |
| `created_after`    | Forms created on or after a date      |
| `updated_before`   | Forms updated on or before a date     |
| `updated_after`    | Forms updated on or after a date      |
| `include_archived` | Include archived (soft-deleted) Forms |

<Warning>
  **⚠️ 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"`).
</Warning>

### Common list patterns

**Active Forms only:**

```javascript JavaScript theme={null}
async function activeForms() {
  return paginate('https://api.vomo.org/v1/forms?include_archived=false');
}
```

**Find Forms by name:**

```javascript JavaScript theme={null}
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 JavaScript theme={null}
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

```bash cURL theme={null}
curl "https://api.vomo.org/v1/forms/789/completions" \
  -H "Authorization: Bearer $VOMO_API_TOKEN"
```

Returns all completions of Form #789, paginated.

### Available filters

| Parameter        | What it filters by                      |
| ---------------- | --------------------------------------- |
| `user_id`        | Only completions by a specific User     |
| `created_before` | Completions created on or before a date |
| `created_after`  | Completions created on or after a date  |
| `updated_before` | Completions updated on or before a date |
| `updated_after`  | Completions updated on or after a date  |

### Common list patterns

**Completions for a specific user across one form:**

```javascript JavaScript theme={null}
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 JavaScript theme={null}
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

```bash cURL theme={null}
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 JavaScript theme={null}
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 JavaScript theme={null}
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 JavaScript theme={null}
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 JavaScript theme={null}
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 JavaScript theme={null}
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 JavaScript theme={null}
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

| Capability                                    | Status                      |
| --------------------------------------------- | --------------------------- |
| Create a Form                                 | Not exposed — admin UI only |
| Update a Form's structure (add/remove fields) | Not exposed                 |
| Delete or archive a Form                      | Not exposed                 |
| Submit a Form Completion on behalf of a user  | Not exposed                 |
| Update an existing Completion's responses     | Not exposed                 |
| Delete a Form Completion                      | Not exposed                 |
| Configure Field options programmatically      | Not 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](/volunteer/workflows/understand-write-limitations).

***

## A reference Forms client

```javascript JavaScript theme={null}
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

<CardGroup cols={2}>
  <Card title="Certificates" icon="certificate" href="/volunteer/concepts/certificates">
    The other read-only resource — training and achievement credentials.
  </Card>

  <Card title="Users" icon="user" href="/volunteer/concepts/users">
    Users complete Forms; the user\_id is the linkage to Form Completions.
  </Card>

  <Card title="Projects and Project Dates" icon="calendar-days" href="/volunteer/concepts/projects-and-project-dates">
    Forms are typically attached to Projects.
  </Card>

  <Card title="The Volunteer Data Model" icon="diagram-project" href="/volunteer/concepts/data-model">
    The full data model context for Forms.
  </Card>
</CardGroup>
