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

# Versioning & Backward Compatibility

> How to build Volunteer integrations that survive API evolution — defensive parsing, the eventual v2 overhaul, deprecation handling, feature flags, and the practices that keep integrations running through change.

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](/volunteer/concepts/data-model), 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 quirk                                                                        | Likely v2 behavior                                               |
| ------------------------------------------------------------------------------- | ---------------------------------------------------------------- |
| `hours` returned as string ("4.00") per audit #40                               | Will be a proper number                                          |
| `verified` returned as 0/1 integer per audit spec                               | Will be a proper boolean                                         |
| `project_id`/`project_date_id` typed string but should be integer per audit #41 | Will be a proper integer                                         |
| `address` typed as array on `ProjectResource` per audit #43                     | Will be a proper object                                          |
| Empty `schema: {}` on Organization endpoints per audit #25/#26/#27              | Will have proper schema with `OrganizationResource`              |
| Path parameter `{completion}` instead of `{completionId}` per audit #20         | Will be renamed for consistency                                  |
| `POST /groups` returns `200` instead of `201` per audit #21                     | Will return proper `201 Created`                                 |
| Anonymous inline body schemas on `POST/PUT /projects` per audit #31/#33         | Will be named, fully-documented component schemas                |
| `operationId: createUser` for upsert endpoint per audit #47                     | Will be renamed to `upsertUser` or split into separate endpoints |
| `name_like` / `email_like` substring matching                                   | May add `name` / `email` exact-match alternatives                |

These are documented in the audit findings throughout the docs (see [Concepts](/volunteer/concepts/data-model) 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 JavaScript theme={null}
// 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 JavaScript theme={null}
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 JavaScript theme={null}
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 JavaScript theme={null}
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 JavaScript theme={null}
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 JavaScript theme={null}
// ❌ 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 JavaScript theme={null}
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 JavaScript theme={null}
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:

| Phase | Customers on v2        | Goal                                              |
| ----- | ---------------------- | ------------------------------------------------- |
| 1     | Internal test          | Verify against test data                          |
| 2     | 1-2 friendly customers | Real-world signals; partners notified             |
| 3     | 10% of customers       | Statistical sample                                |
| 4     | 50% of customers       | Confidence build                                  |
| 5     | All customers          | Full 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.

```sql theme={null}
-- ✅ 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:

```sql theme={null}
-- ✅ 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:

```sql theme={null}
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](https://datatracker.ietf.org/doc/html/rfc8594)), 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 JavaScript theme={null}
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 sunset | Action                        |
| ------------------ | ----------------------------- |
| 90 days            | Acknowledge in planning       |
| 60 days            | Begin migration work          |
| 30 days            | Migration complete and tested |
| 0 days             | Sunset 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 JavaScript theme={null}
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 JavaScript theme={null}
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 JavaScript theme={null}
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:

```markdown theme={null}
# 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 response                   | Best response                                                                 |
| ------------------------------------- | ----------------------------------------------------------------------------- |
| Field is missing (was present before) | Treat as null; log debug                                                      |
| Field's type changed                  | Defensive parser handles it; log warning                                      |
| New required field on writes          | Hopefully 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 changed                | Parser 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:

| Change                           | Defend?                                                                               |
| -------------------------------- | ------------------------------------------------------------------------------------- |
| 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:

| Practice                                                           | What 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 changes                                    | Gradual rollout; easy rollback                |
| Documented assumptions                                             | Roadmap for migration when needed             |
| Per-customer cadence and version                                   | Granular 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

<CardGroup cols={2}>
  <Card title="Sync Architecture Patterns" icon="route" href="/volunteer/best-practices/sync-architecture-patterns">
    The architectural patterns that make change manageable.
  </Card>

  <Card title="Security and Credential Management" icon="lock" href="/volunteer/best-practices/security-and-credential-management">
    The security patterns that complement versioning hygiene.
  </Card>

  <Card title="Data Modeling" icon="diagram-project" href="/volunteer/best-practices/data-modeling">
    The data model that allows partner-side schema evolution.
  </Card>

  <Card title="Error Recovery Patterns" icon="arrows-rotate" href="/volunteer/best-practices/error-recovery-patterns">
    The error-handling patterns that absorb API changes gracefully.
  </Card>
</CardGroup>
