> ## 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 CRM+'s versioning model affects partner integrations — the /v2/ endpoints in the API today, what versioning commitments to assume, and how to write code that survives the platform's evolution.

CRM+ uses a partial-versioning model: most endpoints sit under `/api/` with no version segment, while a specific subset uses `/api/v2/`. There is no `/api/v1/` prefix; the unversioned `/api/` paths are effectively v1, and `/api/v2/` represents newer or improved implementations on a per-endpoint-family basis.

This page covers what's known about the model today, which endpoints use `/v2/`, what backward-compatibility commitments to assume, and how to write integration code that survives the platform's evolution.

## The current versioning state

### What's on `/v2/` today

Two endpoint families currently use the `/v2/` prefix:

| Endpoint family                 | Endpoints                                                                                                                                                                                                                                                |
| ------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Gift Transaction submission** | `POST /api/v2/Gift/Transaction` (single), `POST /api/v2/Gift/Transactions` (batch)                                                                                                                                                                       |
| **Pledge management**           | `POST /api/v2/Pledge` (create), `GET /api/v2/Pledge/{pledgeId}`, `GET /api/v2/Pledge/ByContact/{contactId}`, `POST /api/v2/Pledge/Query`, `GET /api/v2/Pledge/QueryOptions`, `GET /api/v2/Pledge/CustomFields`, `PUT /api/v2/Pledge/WriteOff/{pledgeId}` |

Everything else uses `/api/` with no version segment. The Contact endpoints, the (non-batch, non-v2) Gift endpoints, the Project endpoints, the RecurringGift endpoints, the Webhook endpoints, the Query endpoints for most resources — all unversioned.

### Why `/v2/` exists

The `/v2/` prefix indicates a more recent, sometimes redesigned implementation of an endpoint family. For Gift Transactions, the `/v2/` version is the recommended path for partner integrations submitting gifts. For Pledges, the `/v2/` family is the only version exposed in the current spec.

This means **partner integrations should prefer the `/v2/` variant** when it exists for the operation they're performing. There's no documented reason to call `/api/Gift/Transaction` (without `/v2/`) for a new integration. If a non-v2 variant exists for the same operation, treat the `/v2/` one as canonical.

<Warning>
  The CRM+ spec does not explicitly enumerate the criteria under which `/v2/` is used vs. unversioned `/api/`. The pattern documented here — that `/v2/` is reserved for redesigned families and partner integrations should prefer it — is inferred from the current endpoint inventory. The platform team may evolve the versioning model over time.
</Warning>

***

## Writing code that anticipates `/v2/` evolution

Partner integrations have a long lifespan — measured in years. The current `/v2/` inventory is unlikely to be the final inventory; new families may be promoted to `/v2/` over time, and at some point a `/v3/` may appear. Three patterns help:

### Pattern 1: configurable base path per endpoint family

Don't hardcode `/api/` or `/api/v2/` throughout your codebase. Centralize the path construction:

```javascript JavaScript theme={null}
const CRM_BASE_URL = 'https://api.virtuoussoftware.com';

const ENDPOINTS = {
  contact: '/api/Contact',
  contactQuery: '/api/Contact/Query',
  contactTransaction: '/api/Contact/Transaction',
  gift: '/api/Gift',
  giftTransaction: '/api/v2/Gift/Transaction',    // v2 is the recommended path
  giftQuery: '/api/Gift/Query',
  pledge: '/api/v2/Pledge',                       // entire family is on v2
  pledgeQuery: '/api/v2/Pledge/Query',
  // ...
};

async function virtuousCall(endpointKey, init = {}) {
  const url = `${CRM_BASE_URL}${ENDPOINTS[endpointKey]}`;
  return fetch(url, init);
}
```

When a future spec update moves an endpoint family to a new version, the change is one constant edit instead of search-and-replace across the codebase.

### Pattern 2: feature detection over hardcoded behavior

If two variants of an endpoint exist and the field shapes differ, detect what the platform accepts rather than assuming:

```javascript JavaScript theme={null}
// Cache the feature-detection result per customer
async function supportsBatchGiftTransactions(customerId) {
  const cached = featureCache.get(`${customerId}:batch_gift_tx`);
  if (cached !== undefined) return cached;

  try {
    // Probe with a known-good empty-batch request
    const response = await fetch(
      'https://api.virtuoussoftware.com/api/v2/Gift/Transactions',
      {
        method: 'POST',
        headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
        body: JSON.stringify({ transactions: [] }),
      }
    );
    const supported = response.status !== 404;
    featureCache.set(`${customerId}:batch_gift_tx`, supported);
    return supported;
  } catch {
    return false;
  }
}
```

Useful for endpoints that have phased rollouts or per-customer availability. For the documented, spec-stable endpoints, this is over-engineering — just use them directly.

### Pattern 3: defensive deserialization

Don't assume response payloads have exactly the fields documented in the spec — they may have extras (added in a non-breaking spec update) or omissions (if a field was deprecated). Parse defensively:

```javascript JavaScript theme={null}
// ✅ Good — picks the fields you care about, ignores others
function extractContactSummary(virtuousContact) {
  return {
    id: virtuousContact.id,
    name: virtuousContact.name,
    email: extractPrimaryEmail(virtuousContact),
    modifiedAt: virtuousContact.modifiedDateTimeUtc,
  };
}

// ❌ Bad — strict shape matching that breaks on additions
const ContactSchema = z.object({ /* every documented field */ }).strict();
const contact = ContactSchema.parse(virtuousContact);   // throws on unexpected fields
```

For TypeScript users: define types based on the fields you actually use, not as exhaustive 1:1 mirrors of the spec. Use `Pick<>` types if generating from the spec.

***

## Spec-vs-live field typing

A backward-compatibility concern specific to CRM+: the OpenAPI spec types many fields as `string` that the live API actually accepts (and sometimes returns) as native types — booleans, integers, dates.

| Spec says                                    | Live API accepts                                      | Live API returns                   |
| -------------------------------------------- | ----------------------------------------------------- | ---------------------------------- |
| `string` for booleans like `isPrivate`       | Native `true`/`false` (and string `"true"`/`"false"`) | Native `true`/`false`              |
| `string` for integers like `anniversaryYear` | Native `2010` (and string `"2010"`)                   | Native `2010`                      |
| `string` for dates like `birthDate`          | ISO 8601 date string `"1972-02-19"`                   | ISO 8601 date string               |
| `string` for amounts                         | Native `500.00` or string `"500.00"`                  | Sometimes native, sometimes string |

This mismatch is documented in [Contacts: field typing](/crm/concepts/contacts#updating-contacts), [Donations / Gifts: field typing](/crm/concepts/gifts#field-typing), and elsewhere. The implications for versioning:

* **Don't trust auto-generated SDKs that strictly enforce the spec types.** They'll send `"true"` (string) for booleans and reject `true` (native) in responses, both of which break against the live behavior.
* **Send native types in requests.** They work today and align with what most modern APIs do.
* **Parse defensively in responses.** If you expect a boolean and get a string, coerce; if you expect an integer and get a string number, parse it.

```javascript JavaScript theme={null}
function parseBoolean(value) {
  if (typeof value === 'boolean') return value;
  if (value === 'true' || value === 'True') return true;
  if (value === 'false' || value === 'False') return false;
  return null;
}

function parseAmount(value) {
  if (typeof value === 'number') return value;
  if (typeof value === 'string') return parseFloat(value);
  return null;
}
```

<Note>
  The spec-vs-live typing mismatch is a known issue documented across the audit findings. The platform may eventually update the spec to match live behavior (or vice versa). Until then, write defensive parsing.

  **⚠️ Human input required:** Establish whether the spec or the live API is the canonical source for field types. Either bring the spec in line with live behavior, or document that the spec is the authoritative source and the live API will be aligned to match.
</Note>

***

## Backward compatibility commitments

Without an explicit versioning policy from the platform team, partner integrations should make conservative assumptions about what may and may not change.

### What's safe to assume stable

The following kinds of changes would be backward-breaking and are unlikely to happen silently:

| Stable                                                                                                                            | Why                                                                |
| --------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------ |
| Existing endpoint paths                                                                                                           | Changing a path breaks every integration that uses it.             |
| Existing field names in request bodies                                                                                            | Renaming a field rejects every request that uses the old name.     |
| Existing field names in response bodies                                                                                           | Renaming a field breaks every consumer that reads the old name.    |
| The general request/response envelope structure (`list`/`total` for paginated responses, `error.details[]` for validation errors) | Same reason — wholesale envelope changes break everything.         |
| Authentication mechanism (Bearer tokens, header name)                                                                             | A breaking auth change would be a deliberate, announced migration. |

### What may change without notice

The following kinds of changes are not breaking and partner integrations should expect them to happen:

| May change                                  | Implication                                                  |
| ------------------------------------------- | ------------------------------------------------------------ |
| New fields added to response bodies         | Don't error on unexpected fields.                            |
| New optional fields added to request bodies | Continue sending the fields you've always sent.              |
| New event types added to webhook payloads   | Switch on known event types; ignore unknown.                 |
| New enum values added to existing fields    | Don't validate enums client-side against a hardcoded list.   |
| Error message text changes                  | Switch on status codes and error codes, not message strings. |
| New endpoints added                         | Doesn't affect your existing code.                           |
| Performance characteristics                 | Faster or slower; doesn't affect correctness.                |

### What's ambiguous

A few changes sit in the middle and require explicit confirmation when they happen:

| Ambiguous                                                                   | Resolution                                                                                                                                                   |
| --------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| New required field on an existing request body                              | Breaks existing callers — but might be argued as "the field was always required, just not documented." Treat as breaking and require an announced migration. |
| Status code changes on existing responses (e.g., `200` → `201` on creation) | The audit found a `200`-where-spec-implies-`201` case. Code defensively to handle either.                                                                    |
| Field type changes (string → number on the wire)                            | Currently happens routinely due to the spec-vs-live mismatch. Parse defensively.                                                                             |

***

## Handling deprecated endpoints

If an endpoint is deprecated in a future version, the platform will typically:

1. Add a new endpoint that supersedes it (often under a new version prefix like `/v2/` or `/v3/`).
2. Continue to support the old endpoint for a deprecation window — typically months to years.
3. Eventually remove the old endpoint.

Partner integrations that anticipate this lifecycle handle it cleanly:

### Detect deprecation signals

The platform may emit deprecation warnings via response headers or via documentation. Specifically, watch for:

| Signal                                                  | What to do                                                                          |
| ------------------------------------------------------- | ----------------------------------------------------------------------------------- |
| `Sunset` HTTP header on responses                       | The endpoint will be removed on the date in the header. Plan migration before then. |
| `Deprecation: true` HTTP header                         | The endpoint is deprecated but still functional. Migration recommended.             |
| A spec update marking an endpoint as `deprecated: true` | Same — plan migration.                                                              |
| A platform announcement (changelog, support email)      | Same — plan migration.                                                              |

```javascript JavaScript theme={null}
function checkDeprecationHeaders(response, endpoint) {
  const sunset = response.headers.get('Sunset');
  const deprecation = response.headers.get('Deprecation');

  if (sunset || deprecation) {
    metrics.increment(`virtuous_deprecated_endpoint`, { endpoint, sunset });
    console.warn(`Deprecated endpoint used: ${endpoint}`, { sunset, deprecation });
  }
}
```

Aggregating these warnings into metrics surfaces which deprecated endpoints your integration still uses — the work list for the next migration.

### Migrate proactively

When a `/v2/` (or future-version) variant is introduced for an endpoint family you use, plan migration on a reasonable timeline rather than waiting for sunset:

<Steps>
  <Step title="Read the new endpoint's documentation">
    Confirm what changed — field shapes, semantics, error conditions.
  </Step>

  <Step title="Implement against the new variant in a feature-flagged path">
    Both old and new code paths are present; the flag chooses between them.
  </Step>

  <Step title="Test against the Seeded Sandbox">
    Verify behavior parity between old and new before any customer is migrated.
  </Step>

  <Step title="Migrate customers gradually">
    Per-customer flags let you migrate one customer at a time and roll back if issues surface.
  </Step>

  <Step title="Remove the old code path">
    After all customers have been migrated, delete the old code. Don't leave dead code paths in the codebase.
  </Step>
</Steps>

***

## Defensive coding checklist

A set of practices that make integration code more resilient to platform evolution:

* Endpoint URLs are constants in one place, not hardcoded throughout the codebase.
* Field access is by-name lookup with sensible defaults, not strict shape destructuring.
* Response parsers handle both native and string forms of typed fields (booleans, integers, amounts, dates).
* Webhook event handlers switch on known event types and ignore unknown ones, rather than failing.
* Enum values are not hardcoded for validation — let the API reject invalid values instead of pre-validating against a stale enum list.
* Status code handling is by status class (`2xx`, `4xx`, `5xx`) for retry logic, with explicit branches for known specific codes (`401`, `404`, `409`, `422`, `429`).
* Deprecation headers and `Sunset` headers are logged or metric-counted, not silently ignored.
* SDK code generated from the spec is reviewed before adoption — strict implementations break against the spec-vs-live typing differences.

Following these makes the difference between an integration that needs updates every few months and one that runs unchanged for years.

***

## Where to go next

<CardGroup cols={2}>
  <Card title="Error Recovery Patterns" icon="arrows-rotate" href="/crm/best-practices/error-recovery-patterns">
    The error-handling practices that complement the defensive coding patterns on this page.
  </Card>

  <Card title="Base URLs and Environments" icon="server" href="/crm/base-urls">
    The reference for the host-level URL structure and how `/v2/` paths fit into it.
  </Card>

  <Card title="Sync Architecture Patterns" icon="diagram-project" href="/crm/best-practices/sync-architecture-patterns">
    The architectural patterns that the migration approach on this page builds on.
  </Card>

  <Card title="Changelog" icon="clock-rotate-left" href="/crm/changelog">
    The platform's evolution log — watch for deprecation announcements and new versioned endpoints.
  </Card>
</CardGroup>
