Walk through reading Form Completion data — paginating through a Form’s completions, filtering by user or date range, joining responses with field definitions for meaningful display, and the patterns for syncing completion data to external systems.
When volunteers fill out a Form in VOMO — a waiver, a signup question, a post-event survey — the result is a Form Completion record. Form Completions are read-only via the API (volunteers submit them through VOMO; partner integrations consume them), but they’re often the richest source of structured data about volunteers’ preferences, eligibility, and feedback.This workflow page walks through reading Form Completions: how to find them, how to display them with their field context, how to filter by user or time range, and how to sync them to external systems for reporting or follow-up.If you haven’t yet, skim the Forms and Form Completions concept page for the five-resource family structure and field shapes.
List of Forms (to find the Form ID you care about)
GET /forms/{id}/completions
List of Completions for that Form (filterable)
GET /forms/{id}/completions/{completion}
A single Completion with full field responses
⚠️ Spec gap (audit #19, #20): The path parameter for the single-completion endpoint is {completion} (not the more conventional {completionId}), and the parameter descriptions in the spec contain typos (“VOMO From ID” instead of “VOMO Form ID”). The endpoints work as documented; the naming will be cleaned up in a future spec revision.
The Form Completion’s field_responses array contains the volunteer’s answers, but each response is just { field_id, value } — without the Form’s field definitions, you don’t know what question was asked or what type of input the value represents.The pattern: fetch the Form once, then join responses with field definitions when displaying:
JavaScript
async function readCompletionWithContext(formId, completionId) { // 1. Fetch the Form (gives us field definitions) const formResponse = await fetch( `https://api.vomo.org/v1/forms?name_like=`, // workaround — see notes { headers: { Authorization: `Bearer ${token}` } } ); // ... locate the specific form const form = (await formResponse.json()).data.find((f) => f.id === formId); if (!form) throw new Error(`Form ${formId} not found`); // 2. Fetch the Completion const completionResponse = await fetch( `https://api.vomo.org/v1/forms/${formId}/completions/${completionId}`, { headers: { Authorization: `Bearer ${token}` } } ); if (!completionResponse.ok) { throw new Error(`Failed: ${completionResponse.status}`); } const completion = (await completionResponse.json()).data; // 3. Build a field lookup const fieldsById = new Map(form.fields.map((f) => [f.id, f])); // 4. Join responses with field definitions return { completionId: completion.id, userId: completion.user_id, projectId: completion.project_id, submittedAt: new Date(completion.created_at), responses: completion.field_responses.map((response) => { const field = fieldsById.get(response.field_id); return { fieldId: response.field_id, question: field?.name ?? `Field ${response.field_id}`, fieldType: field?.field_type ?? 'UNKNOWN', rawValue: response.value, displayValue: parseFieldValue(field, response), }; }), };}
The output is a displayable Completion — question text, field type, and parsed value per response — suitable for showing in a UI: “Bruce Wayne’s General Volunteer Waiver submitted 2025-04-19.”
function parseFieldValue(field, response) { if (!field) return response.value; switch (field.field_type) { case 'SHORTTEXT': case 'LONGTEXT': case 'TEXTSINGLE': return response.value; case 'WAIVER': // Truthy if "true" / "1" — treat as accepted return response.value === 'true' || response.value === '1'; case 'DROPDOWN': // Look up the option by value to get display label const option = field.options?.find((o) => o.value === response.value); return option?.name ?? response.value; case 'MULTIPLESELECT': // Comma-separated values; look up each const values = response.value.split(',').map((v) => v.trim()).filter(Boolean); return values.map((v) => { const opt = field.options?.find((o) => o.value === v); return opt?.name ?? v; }); default: return response.value; }}
The exact storage format for MULTIPLESELECT (comma-separated vs. JSON array) isn’t documented in the spec — the pattern above handles the most likely format defensively.
For a partner UI showing “all forms this volunteer has submitted”:
JavaScript
async function getUserFormHistory(userId) { // 1. Get all active Forms (so we know what to check) const formsResponse = await fetch( 'https://api.vomo.org/v1/forms?include_archived=false', { headers: { Authorization: `Bearer ${token}` } } ); const forms = (await formsResponse.json()).data; // 2. For each Form, find this user's completions const allHistory = []; for (const form of forms) { const userCompletions = await listCompletions(form.id, { user_id: userId.toString(), }); for (const completion of userCompletions) { allHistory.push({ formId: form.id, formName: form.name, completionId: completion.id, submittedAt: new Date(completion.created_at), projectId: completion.project_id, }); } } // 3. Sort newest first return allHistory.sort((a, b) => b.submittedAt - a.submittedAt);}
This is N+1 (one Forms 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:
Limit to specific forms the user is likely to have completed (waivers, role-specific forms)
Cache the result per user (Form history changes only when new completions arrive)
For organizations requiring annual waiver renewal:
JavaScript
async function findUsersNeedingWaiverRenewal(waiverFormId) { const oneYearAgo = new Date(); oneYearAgo.setFullYear(oneYearAgo.getFullYear() - 1); // 1. Get all waiver completions const allCompletions = await listCompletions(waiverFormId); // 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 (or who haven't signed) const needsRenewal = []; for (const [userId, completion] of latestByUser.entries()) { if (new Date(completion.created_at) < oneYearAgo) { needsRenewal.push({ userId, lastSignedAt: new Date(completion.created_at), daysSinceSign: Math.floor( (Date.now() - new Date(completion.created_at)) / (1000 * 60 * 60 * 24) ), }); } } return needsRenewal;}
Feed the result into the customer’s renewal outreach pipeline. Run on a schedule (weekly or monthly) and email/notify users approaching their renewal deadline.
Scenario 3: Sync completions to an external system
For partner integrations mirroring Form data into an external CRM or analytics platform:
JavaScript
async function syncFormCompletionsToExternal(customerId, formId) { const lastSync = await getCheckpoint(customerId, `form_${formId}`); const reader = new FormCompletionReader({ token }); const newCompletions = await listCompletions(formId, { created_after: lastSync.toISOString(), }); let highWaterMark = lastSync; for (const completion of newCompletions) { try { // Read with field context for richer external data const enriched = await reader.readCompletion(formId, completion.id); await externalSystem.recordCompletion({ externalId: `vomo-completion-${completion.id}`, formId, userId: completion.user_id, projectId: completion.project_id, submittedAt: new Date(completion.created_at), responses: enriched.responses, }); const createdAt = new Date(completion.created_at); if (createdAt > highWaterMark) highWaterMark = createdAt; } catch (err) { console.error(`Failed to sync completion ${completion.id}:`, err); // Continue to next — partial failures shouldn't block the whole sync } } await advanceCheckpoint(customerId, `form_${formId}`, highWaterMark);}
Run on a schedule per Form the customer wants synced. The high-water-mark pattern (advancing to the latest created_at actually seen) ensures interrupted syncs resume correctly.
If a partner integration needs to push externally-collected Form data into VOMO (e.g., waivers signed through a third-party legal service), the typical path is:
Capture the completion data externally
Store a reference linking the external completion to the VOMO User
Coordinate with the customer’s admin team for periodic CSV import into VOMO — or skip the VOMO sync entirely if the external system is authoritative