Skip to main content
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 familyEndpoints
Gift Transaction submissionPOST /api/v2/Gift/Transaction (single), POST /api/v2/Gift/Transactions (batch)
Pledge managementPOST /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.
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.

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
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
// 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
// ✅ 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 saysLive API acceptsLive API returns
string for booleans like isPrivateNative true/false (and string "true"/"false")Native true/false
string for integers like anniversaryYearNative 2010 (and string "2010")Native 2010
string for dates like birthDateISO 8601 date string "1972-02-19"ISO 8601 date string
string for amountsNative 500.00 or string "500.00"Sometimes native, sometimes string
This mismatch is documented in Contacts: field typing, Donations / 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
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;
}
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.

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:
StableWhy
Existing endpoint pathsChanging a path breaks every integration that uses it.
Existing field names in request bodiesRenaming a field rejects every request that uses the old name.
Existing field names in response bodiesRenaming 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 changeImplication
New fields added to response bodiesDon’t error on unexpected fields.
New optional fields added to request bodiesContinue sending the fields you’ve always sent.
New event types added to webhook payloadsSwitch on known event types; ignore unknown.
New enum values added to existing fieldsDon’t validate enums client-side against a hardcoded list.
Error message text changesSwitch on status codes and error codes, not message strings.
New endpoints addedDoesn’t affect your existing code.
Performance characteristicsFaster or slower; doesn’t affect correctness.

What’s ambiguous

A few changes sit in the middle and require explicit confirmation when they happen:
AmbiguousResolution
New required field on an existing request bodyBreaks 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., 200201 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:
SignalWhat to do
Sunset HTTP header on responsesThe endpoint will be removed on the date in the header. Plan migration before then.
Deprecation: true HTTP headerThe endpoint is deprecated but still functional. Migration recommended.
A spec update marking an endpoint as deprecated: trueSame — plan migration.
A platform announcement (changelog, support email)Same — plan migration.
JavaScript
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:
1

Read the new endpoint's documentation

Confirm what changed — field shapes, semantics, error conditions.
2

Implement against the new variant in a feature-flagged path

Both old and new code paths are present; the flag chooses between them.
3

Test against the Seeded Sandbox

Verify behavior parity between old and new before any customer is migrated.
4

Migrate customers gradually

Per-customer flags let you migrate one customer at a time and roll back if issues surface.
5

Remove the old code path

After all customers have been migrated, delete the old code. Don’t leave dead code paths in the codebase.

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

Error Recovery Patterns

The error-handling practices that complement the defensive coding patterns on this page.

Base URLs and Environments

The reference for the host-level URL structure and how /v2/ paths fit into it.

Sync Architecture Patterns

The architectural patterns that the migration approach on this page builds on.

Changelog

The platform’s evolution log — watch for deprecation announcements and new versioned endpoints.
Last modified on May 27, 2026