Skip to main content
The Volunteer API is at v1 — URL-versioned via the /v1/ segment in the base URL (https://api.vomo.org/v1). A v2 overhaul will eventually arrive, addressing many of the audit-flagged quirks (type inconsistencies, naming inconsistencies, anonymous schemas) accumulated in v1. When that happens, integrations that handled v1’s quirks defensively will migrate gracefully; integrations that depended on v1’s exact behavior will break. This page covers the patterns that prepare integrations for API evolution: defensive parsing, version-aware clients, feature flags for behavior changes, deprecation handling, and the partner-side schema evolution practices that keep integrations running through inevitable change.

The eventual v2 reality

The current v1 API has documented quirks that v2 will likely address:
v1 quirkLikely v2 behavior
hours returned as string (“4.00”) per audit #40Will be a proper number
verified returned as 0/1 integer per audit specWill be a proper boolean
project_id/project_date_id typed string but should be integer per audit #41Will be a proper integer
address typed as array on ProjectResource per audit #43Will be a proper object
Empty schema: {} on Organization endpoints per audit #25/#26/#27Will have proper schema with OrganizationResource
Path parameter {completion} instead of {completionId} per audit #20Will be renamed for consistency
POST /groups returns 200 instead of 201 per audit #21Will return proper 201 Created
Anonymous inline body schemas on POST/PUT /projects per audit #31/#33Will be named, fully-documented component schemas
operationId: createUser for upsert endpoint per audit #47Will be renamed to upsertUser or split into separate endpoints
name_like / email_like substring matchingMay add name / email exact-match alternatives
These are documented in the audit findings throughout the docs (see Concepts and individual workflow pages). Many integrations have built workarounds for them; v2 will allow those workarounds to be removed. But between now and v2, your integration runs against v1. And during the transition, your integration may need to support both. This page is about how.

Principle 1: defensive parsing at every boundary

The single most valuable practice for surviving API evolution: never trust the API’s response shape. Parse defensively at the boundary; produce a clean internal representation; quarantine the API’s quirks in one place.

The pattern

JavaScript
// Boundary parser — knows about the API's quirks
class VomoUserParser {
  parseFromApi(raw) {
    return {
      id: this._parseInt(raw.id),
      firstName: raw.first_name ?? null,
      lastName: raw.last_name ?? null,
      fullName: raw.full_name ?? null,
      email: this._parseEmail(raw.email),
      phone: raw.phone ?? null,
      birthday: this._parseDate(raw.birthday),
      gender: raw.gender ?? null,
      userStatus: raw.user_status ?? null,
      membershipRole: raw.membership_role ?? null,
      createdAt: this._parseDateTime(raw.created_at),
      updatedAt: this._parseDateTime(raw.updated_at),
    };
  }

  _parseInt(value) {
    if (value === null || value === undefined) return null;
    if (typeof value === 'number') return value;
    return parseInt(value, 10) || null; // Audit #41: handle string ints
  }

  _parseDateTime(value) {
    if (!value) return null;
    const parsed = new Date(value);
    if (isNaN(parsed.getTime())) return null;
    return parsed;
  }

  _parseDate(value) {
    if (!value) return null;
    // Audit #44: format is "YYYY-MM-DD"
    const parsed = new Date(value + 'T00:00:00Z');
    if (isNaN(parsed.getTime())) return null;
    return parsed;
  }

  _parseEmail(value) {
    return value?.toLowerCase().trim() ?? null;
  }
}
The parser is the only place that knows about v1’s quirks. Application code receives clean, typed, normalized data — and doesn’t care whether the API returns hours as a string or number.

When v2 ships

JavaScript
class VomoUserParserV2 extends VomoUserParser {
  // v2 returns proper types; the base parser's defensive coercion still works
  // but we can override for clarity if desired

  _parseInt(value) {
    return value; // v2 returns proper integers; pass through
  }
}
The migration is localized — only the parser changes. Application code continues working without touching.

Principle 2: version-aware clients

Plan for v1 and v2 to coexist during the transition. Use a version-aware client that knows about both:
JavaScript
class VomoClient {
  constructor({ token, version = 'v1' }) {
    this.token = token;
    this.version = version;
    this.baseUrl = `https://api.vomo.org/${version}`;
  }

  async get(path) {
    return fetch(`${this.baseUrl}${path}`, {
      headers: { Authorization: `Bearer ${this.token}` },
    });
  }
}

// Default to v1 for now
const client = new VomoClient({ token, version: 'v1' });

// When v2 is available and tested
const v2Client = new VomoClient({ token, version: 'v2' });

Per-customer version selection

For staged migration, version selection becomes per-customer:
JavaScript
async function getClientForCustomer(customerId) {
  const config = await db.getCustomerConfig(customerId);
  const version = config.vomoApiVersion ?? 'v1';
  const token = await credentialStore.getToken(customerId, 'api_call');
  return new VomoClient({ token, version });
}
Some customers can be on v2 while others stay on v1, allowing gradual migration. The application code stays oblivious — the client and parser handle the version differences.

Principle 3: ignore unknown fields

When the API returns fields you don’t recognize, ignore them. Don’t fail; don’t log them as errors; just skip:
JavaScript
class TolerantParser {
  parse(raw) {
    const known = {
      id: raw.id,
      first_name: raw.first_name,
      // ... fields you explicitly know about ...
    };

    // Unknown fields are silently ignored
    return known;
  }
}
If the API adds a new field (e.g., v2 adds last_login_at), your integration continues working — it just doesn’t surface the new field until you decide to. This is the forward-compatible reading principle: be conservative in what you send, liberal in what you accept.

Don’t fail on additions

The wrong thing to do:
JavaScript
// ❌ Anti-pattern
class StrictParser {
  parse(raw) {
    const known = ['id', 'first_name', 'last_name', 'email'];
    const unexpected = Object.keys(raw).filter((k) => !known.includes(k));
    if (unexpected.length > 0) {
      throw new Error(`Unknown fields: ${unexpected.join(',')}`);
    }
    return raw;
  }
}
This breaks every time the API adds a new field. Customers see the integration fail; the partner has to ship a release just to acknowledge the new field exists.

Optional: log unknown fields for awareness

For debugging help (during v2 transition especially), log unknown fields at a low level:
JavaScript
class AwareParser {
  parse(raw, knownFields) {
    const unknown = Object.keys(raw).filter((k) => !knownFields.includes(k));
    if (unknown.length > 0) {
      logger.debug('Unknown fields encountered', { fields: unknown });
    }
    return this._extractKnown(raw, knownFields);
  }
}
The log entry doesn’t fail the operation but surfaces what’s new. Useful for awareness; not load-bearing.

Principle 4: feature flags for API behavior changes

When migrating from v1 to v2 (or any time you change how the integration handles API responses), wrap the change in a feature flag:
JavaScript
async function fetchUserDetail(customerId, userId) {
  const useV2 = await featureFlags.isEnabled('use_vomo_v2', { customerId });
  const client = useV2
    ? new VomoClient({ version: 'v2', token: await getToken(customerId) })
    : new VomoClient({ version: 'v1', token: await getToken(customerId) });

  const response = await client.get(`/users/${userId}`);
  const raw = await response.json();

  return useV2
    ? new VomoUserParserV2().parseFromApi(raw.data)
    : new VomoUserParser().parseFromApi(raw.data);
}
Flagged rollout:
PhaseCustomers on v2Goal
1Internal testVerify against test data
21-2 friendly customersReal-world signals; partners notified
310% of customersStatistical sample
450% of customersConfidence build
5All customersFull migration; v1 client code marked for removal
At each phase, monitor: error rates, processed-record counts, reconciliation gap rates. If anything degrades, roll back the flag (no code deploy needed).

Principle 5: schema evolution on the partner side

Your own data model evolves too — fields get added, types change, columns are deprecated. Plan for the partner-side schema to evolve in lockstep with VOMO’s changes:

Pattern: additive changes only

Whenever possible, add columns; don’t remove them. Removing a column breaks queries that depend on it.
-- ✅ Additive: new column
ALTER TABLE persons ADD COLUMN preferred_pronouns VARCHAR;

-- ⚠ Risky: removed column
ALTER TABLE persons DROP COLUMN deprecated_field;  -- Anything depending on this breaks
Removal should follow a deprecation cycle: mark as deprecated, migrate consumers off, then remove.

Pattern: nullable new columns

New columns should be nullable initially:
-- ✅ Safe to deploy: existing rows have NULL for the new field
ALTER TABLE persons ADD COLUMN ethnicity VARCHAR DEFAULT NULL;

-- ❌ Risky: NOT NULL constraint breaks if any row already exists
ALTER TABLE persons ADD COLUMN ethnicity VARCHAR NOT NULL;
Backfill data before adding NOT NULL. Or accept nullable forever — sometimes the right answer.

Pattern: schema versioning

For complex schemas, track the migration version:
CREATE TABLE schema_migrations (
  version VARCHAR PRIMARY KEY,
  applied_at TIMESTAMP DEFAULT NOW()
);
This is a standard pattern (and most ORMs handle it). It lets you reason about which version of the schema is currently deployed in each environment.

Principle 6: deprecation handling

When VOMO eventually marks a field or endpoint as deprecated (typical pattern: Deprecation header or Sunset header per RFC 8594), your integration should:
  1. Detect the deprecation in API responses
  2. Log it for awareness
  3. Schedule migration before the sunset date
  4. Verify the migration before the sunset takes effect

Pattern: detect deprecation headers

JavaScript
async function fetchWithDeprecationDetection(url, options) {
  const response = await fetch(url, options);

  const deprecation = response.headers.get('Deprecation');
  const sunset = response.headers.get('Sunset');
  const link = response.headers.get('Link'); // Often points to migration docs

  if (deprecation) {
    await db.upsert('api_deprecation_signals', {
      url,
      deprecation_header: deprecation,
      sunset_date: sunset ? new Date(sunset) : null,
      migration_link: link,
      first_seen_at: new Date(),
    });

    // Alert ops if a deprecation we haven't planned for surfaces
    const planned = await db.isDeprecationPlanned(url);
    if (!planned) {
      await alertOps({
        severity: 'low',
        type: 'unhandled_deprecation',
        url,
        sunset,
      });
    }
  }

  return response;
}
A central record of all observed deprecations becomes the migration backlog.

Pattern: scheduled migration with safety margin

For a deprecated endpoint with a sunset date, migrate well before:
Time before sunsetAction
90 daysAcknowledge in planning
60 daysBegin migration work
30 daysMigration complete and tested
0 daysSunset takes effect
If you wait until the last 30 days, you’re under pressure and prone to bugs. Earlier migration leaves room to handle surprises.

Principle 7: contract testing

For partner integrations, run regular tests against VOMO that verify the integration’s assumptions about response shapes:
JavaScript
describe('VOMO API contract — Users', () => {
  it('returns expected fields on list', async () => {
    const response = await fetch(
      'https://api.vomo.org/v1/users?page=1',
      { headers: { Authorization: `Bearer ${testToken}` } }
    );
    const page = await response.json();

    expect(page).toHaveProperty('data');
    expect(page).toHaveProperty('links.next');
    expect(page).toHaveProperty('meta.total');

    if (page.data.length > 0) {
      const sampleUser = page.data[0];
      expect(sampleUser).toHaveProperty('id');
      expect(sampleUser).toHaveProperty('email');
      expect(sampleUser).toHaveProperty('updated_at');
    }
  });

  it('updated_at is parseable as ISO 8601 datetime', async () => {
    const response = await fetch(/* ... */);
    const page = await response.json();
    if (page.data.length > 0) {
      const parsed = new Date(page.data[0].updated_at);
      expect(parsed.toString()).not.toBe('Invalid Date');
    }
  });

  it('hours field on participations is parseable as float', async () => {
    // The known v1 quirk per audit #40
    const userDetail = await fetchUserDetail(/* ... */);
    if (userDetail.participations?.length > 0) {
      const hours = userDetail.participations[0].hours;
      expect(typeof hours === 'string' || typeof hours === 'number').toBe(true);
      expect(parseFloat(hours)).toBeGreaterThanOrEqual(0);
    }
  });
});
Run these tests:
  • Nightly against the production API (with a test customer’s token)
  • As part of CI for any release that touches the integration
When VOMO ships v2, these tests will surface incompatibilities — expect(typeof hours === 'number') would now pass without the string fallback. You’d see the change immediately and could plan the migration.

Don’t test in production with real data

Use a dedicated test customer / test organization for contract tests. Don’t run them against a real customer’s data — both for privacy and for stability (test runs could affect real data if the API behaves unexpectedly).

Principle 8: graceful handling of schema differences

Your integration’s schema and the API’s schema will inevitably diverge. Build the partner side to accommodate either:

Pattern: optional source fields

If a downstream system requires a field that VOMO sometimes returns as null:
JavaScript
function transformForExternal(vomoUser) {
  return {
    external_id: `vomo-${vomoUser.id}`,
    email: vomoUser.email,
    first_name: vomoUser.first_name ?? 'Unknown',  // Fallback for null
    last_name: vomoUser.last_name ?? '',
    phone: vomoUser.phone || null,                  // Convert empty string to null
    // ...
  };
}
The fallbacks let your integration produce valid output even when VOMO provides partial data.

Pattern: rejection vs. degradation

For records that can’t be sanely processed (missing required fields, malformed data), choose between rejection (DLQ) and degradation (process with defaults):
JavaScript
function processWithFallback(vomoUser) {
  // Required: email and ID
  if (!vomoUser.email || !vomoUser.id) {
    return { skipped: true, reason: 'missing_required_fields' };
  }

  // Everything else has defaults
  return externalSystem.upsertUser({
    external_id: `vomo-${vomoUser.id}`,
    email: vomoUser.email.toLowerCase(),
    first_name: vomoUser.first_name?.trim() || 'Unknown',
    last_name: vomoUser.last_name?.trim() || '',
    // ...
  });
}
Decide per-field whether absence is fatal or tolerable. Document the decisions.

Principle 9: documentation of assumptions

Every assumption your integration makes about the API should be documented. When the API changes, the documentation tells you what to re-verify. A simple convention: a per-integration API_ASSUMPTIONS.md file:
# Integration Assumptions about VOMO API v1

## Identifiers
- `user.id` is integer
- `project.id` is integer
- Participation primary key: (project_date_id, user_id)

## Field types
- `hours` returned as string (audit #40); we parse as float
- `verified` returned as 0/1 integer; we treat as boolean
- `birthday` returned as "YYYY-MM-DD" string

## Endpoints we depend on
- GET /users (list with updated_after filter)
- GET /users/{id} (detail with embedded participations)
- POST /users (upsert by email)
- GET /projects (list with active/published/dates filters)
- GET /projects/{id} (detail with embedded all_dates)
- GET /projects/date/{id} (detail with embedded participants)

## Pagination
- Page size: 15 (not configurable)
- Page parameter: `page=N`
- Next link in response.links.next; null when no next page

## Rate limits
- We throttle to 3 req/sec per customer
- We respect Retry-After on 429
When a v2 migration is on the horizon, this doc is the migration checklist — every assumption that v2 changes needs corresponding integration code update.

Principle 10: graceful failure modes for API changes

When the API does something unexpected, what does your integration do?
Unexpected responseBest response
Field is missing (was present before)Treat as null; log debug
Field’s type changedDefensive parser handles it; log warning
New required field on writesHopefully introduced with deprecation notice; otherwise fail with clear error
Endpoint returns 404 (was 200 before)Treat as resource-not-found; reconciliation will surface if needed
Endpoint moved (new URL)Hopefully deprecated first; otherwise discovered via failed contract test
Response shape changedParser fails; record is sent to DLQ for review
The general principle: fail loudly enough to be detected, but quietly enough to not crash the whole integration. Per-record failures go to DLQ; systemic patterns trigger alerts; the integration continues serving the customers it can.

Migration playbook for v2

When VOMO v2 lands, the migration follows a predictable pattern:

Phase 1: assess (week 0-2)

  • Review v2’s changes against API_ASSUMPTIONS.md
  • Categorize: backward-compatible additions, type fixes, structural changes
  • Estimate effort per category

Phase 2: prepare (week 2-6)

  • Update the parser layer to handle both v1 and v2 response shapes
  • Add feature flag for v2 client selection per customer
  • Update contract tests to verify v2 shapes
  • Run contract tests against v2 with a test customer

Phase 3: pilot (week 6-8)

  • Migrate internal test customer to v2
  • Run for 1-2 weeks
  • Monitor: error rates, reconciliation gaps, processing rates

Phase 4: progressive rollout (week 8-20)

  • 1 friendly customer → 10% → 50% → 100%
  • At each step, monitor metrics for ~1 week before advancing

Phase 5: cleanup (week 20+)

  • Remove v1 code paths
  • Update API_ASSUMPTIONS.md
  • Update documentation
  • Celebrate
The timeline scales with integration complexity. A simple read-only sync may compress to weeks; a complex bidirectional integration may take months.

What’s worth defending against vs. accepting

Not every potential API change deserves defensive code. The pragmatic decision matrix:
ChangeDefend?
Type change on a field you parse✓ Always — parser handles both
New field added✓ Ignore unknown fields by default
Existing field removed✗ Hard to defend against without complicating code; usually announced via deprecation
Endpoint path renamed✗ Usually deprecated with warning; migration is straightforward
Pagination semantics change✓ Defensive — handle both old and new link/meta shapes
Authentication scheme change✗ Major version change; full migration
Rate limit semantics change✓ Defensive — handle both Retry-After and other backoff signals
The general rule: defend against subtle changes that could silently produce wrong output; accept the cost of explicit migration for major changes that are obvious when they happen.

Final principle: design for change

The deepest defense against API evolution is making the integration easy to change. Practices that support this:
PracticeWhat it gives you
Strong separation of layers (API client / parser / business logic)Changes in one don’t cascade
Comprehensive tests (unit, integration, contract)Confidence in refactoring
Feature flags for risky changesGradual rollout; easy rollback
Documented assumptionsRoadmap for migration when needed
Per-customer cadence and versionGranular migration without “big bang” deploys
Operational maturity (monitoring, alerts, playbooks)Catch regressions fast
An integration with these practices doesn’t dread API changes — it absorbs them. An integration without them treats every change as a crisis. The difference is months of cumulative engineering work, paid out one improvement at a time.

Where to go next

Sync Architecture Patterns

The architectural patterns that make change manageable.

Security and Credential Management

The security patterns that complement versioning hygiene.

Data Modeling

The data model that allows partner-side schema evolution.

Error Recovery Patterns

The error-handling patterns that absorb API changes gracefully.
Last modified on May 22, 2026