Skip to main content
The Raise API has an unusual versioning story for an actively-evolving platform: there is no version segment in the URL path. Every endpoint lives at /api/{Resource}/... — no /v1/, no /v2/. This isn’t an oversight; it’s the platform’s current shape, and partner integrations should design around it. This page covers what the unversioned namespace means for partner integrations, the defensive coding patterns that handle change gracefully, what to expect from future API evolution, and the practical work that keeps an integration durable across years of platform development. The audience is integration architects making long-term durability decisions and engineers writing the code that will be running against Raise five years from now.

The unversioned reality

Every Raise API path begins with /api/:
GET /api/Donor/list
POST /api/Raise/give
POST /api/Webhook
PUT /api/RecurringGift/{id}/cancel
GET /api/Query/options/{queryType}
No /v1/, no /v2/. The implications:
ImplicationWhat it means
Changes apply to all clients at onceWhen Raise updates an endpoint’s behavior, every integration sees the change simultaneously. No “stay on v1 until you’re ready” option.
Breaking changes are platform-level decisionsIf a field is renamed, every integration must adapt. The platform team coordinates this carefully.
New fields appear without integration code changesAdditive changes to response shapes don’t break clients that ignore unknown fields.
Defensive coding is non-optionalAn integration that assumes the spec it was built against is the spec forever will eventually break.
The platform’s incentive structure pushes strongly toward backward-compatible changes — breaking changes affect every integration simultaneously, which is expensive for everyone. In practice, most changes are additive: new fields, new endpoints, new optional parameters. Breaking changes are rare and announced in advance.

What the OpenAPI spec actually represents

The Raise OpenAPI spec is the platform’s documented contract, but it’s not always identical to the live API’s behavior. A few realities partner integrations should understand:

The spec lags the live API

Spec updates lag platform changes. A new field added to a response may take weeks to appear in the spec; a new endpoint may exist in production before it’s documented. This means the live API is sometimes a superset of what the spec describes. For partner integrations, the implication is don’t blindly trust the spec as exhaustive. Build flexibility for fields and behaviors that the spec doesn’t yet document.

The spec sometimes lags the live API in the other direction

Less commonly, the spec may document fields or behaviors that don’t yet exist in production — features that are planned but not deployed. Treat the spec as a contract for what should work, but verify against live data when behaviors are critical.

Integer enums often lack labels

Throughout the Raise spec, integer enums appear without labels: QueryOperator, Frequency, EventType, Status, Format, ChargeStatus, and others. The spec exposes the integer set but doesn’t document what each integer means. This is a documentation gap, not a contract gap — the values are stable. Partner integrations should:
  • Discover the integer-to-label mapping through documented discovery paths (e.g., eventTypeDisplay on WebhookLogListModel, statusDisplay on various resources, GET /api/Query/options/{queryType}).
  • Cache the discovered mapping.
  • Treat unknown integer values gracefully (don’t crash on a new integer that wasn’t there at integration build time).
See Statuses and Lifecycle States for the full landscape of integer enums and the discovery patterns.

Optional fields may be missing

The spec marks some fields as optional. In practice, optional fields can:
  • Be absent from the response entirely (no key in the JSON).
  • Be present with a null value.
  • Be present with a default value (often empty string or 0).
Partner integration code should handle all three:
JavaScript
// ❌ Fragile — crashes if donor.address is missing or null
const city = donor.address.city;

// ✅ Defensive — works in all three cases
const city = donor.address?.city ?? null;
The optional-chaining + null-coalescing pattern (or equivalent in other languages) is essential.

Defensive coding patterns

The patterns below make integration code resilient to the changes that will inevitably happen.

Pattern 1: ignore unknown fields

When deserializing API responses, ignore fields the integration doesn’t know about. Don’t fail on new fields that appear over time:
JavaScript
// ✅ JSON.parse naturally ignores fields not used by your code
const gift = await response.json();
const amount = gift.amount;  // Works whether the response has 50 fields or 60

// ❌ Don't strictly validate against a known field set
function strictParseGift(json) {
  const KNOWN_FIELDS = ['id', 'amount', 'date', ...];
  const parsed = JSON.parse(json);
  for (const key of Object.keys(parsed)) {
    if (!KNOWN_FIELDS.includes(key)) throw new Error(`Unknown field: ${key}`);
  }
  return parsed;
}
For strictly-typed languages, mark the model as allowing extra fields:
# Python with Pydantic
class GiftModel(BaseModel):
    id: int
    amount: float
    date: datetime
    # ... other known fields ...

    class Config:
        extra = "allow"  # Accept and preserve unknown fields
// TypeScript — define a known interface but accept additional properties
interface Gift {
  id: number;
  amount: number;
  date: string;
  // ... other known fields ...
  [key: string]: unknown;  // Accept additional fields
}

Pattern 2: handle unknown enum values gracefully

When the integration encounters an integer enum value it doesn’t recognize, log it and continue rather than crashing:
JavaScript
const KNOWN_FREQUENCIES = {
  1: 'Annual',
  2: 'Semi-annual',
  4: 'Quarterly',
  12: 'Monthly',
  26: 'Biweekly',
  52: 'Weekly',
  100: 'Continuous',
};

function formatFrequency(integer) {
  const label = KNOWN_FREQUENCIES[integer];
  if (!label) {
    console.warn(`Unknown frequency integer: ${integer}`);
    return `Frequency ${integer}`;  // Fallback label
  }
  return label;
}
The fallback is friendly to the user — “Frequency 5” tells them something is there, even if your integration doesn’t know what. Logging the unknown value alerts the integration team to add support for it. For business-logic decisions that branch on enum values, treat unknown values as the most conservative branch:
JavaScript
function shouldChargeSchedule(schedule) {
  const ACTIVE = 1;
  const CANCELLED = 2;
  const PAUSED = 3;

  if (schedule.status === ACTIVE) return true;
  if (schedule.status === CANCELLED || schedule.status === PAUSED) return false;

  // Unknown status — assume not chargeable to be safe
  console.warn(`Unknown schedule status: ${schedule.status}`);
  return false;
}
Defaulting to “not chargeable” is safer than defaulting to “chargeable” — the former produces a missed charge that can be fixed, the latter produces a wrong charge that requires a refund.

Pattern 3: handle new fields opportunistically

When the live API starts returning new fields the integration could use, the integration can adopt them at its own pace:
JavaScript
function mapGiftForDestination(gift) {
  const base = {
    raise_id: gift.id,
    amount: gift.amount,
    date: gift.date,
    donor_email: gift.donor?.email,
  };

  // Newer field — use if present, otherwise fall back
  if (gift.processingFee !== undefined) {
    base.processing_fee = gift.processingFee;
  }

  // Even newer field — use if present
  if (gift.netRevenue !== undefined) {
    base.net_revenue = gift.netRevenue;
  }

  return base;
}
This pattern means the integration gracefully adopts new fields as they appear, without requiring a code change for each one. The downstream system gets richer data over time without integration team intervention.

Pattern 4: feature-detect rather than version-detect

Without a version segment to switch on, integrations can’t say “if v2, do this; if v1, do that.” Instead, detect features by what fields or endpoints exist:
JavaScript
async function checkFeatureSupport() {
  // Does the gift query response support a `processingFee` field?
  const sample = await fetch(
    'https://prod-api.raisedonors.com/api/Gift/list?Take=1',
    { headers: { Authorization: `Bearer ${token}` } }
  ).then((r) => r.json());

  const supportsFeeData = sample.items[0] && 'processingFee' in sample.items[0];

  return { supportsFeeData };
}

const features = await checkFeatureSupport();

if (features.supportsFeeData) {
  // Use the new fee data
}
Feature detection is more robust than version detection — it asks “is this thing available?” rather than “what version am I on?” The former adapts naturally to platform changes; the latter doesn’t.

Pattern 5: defensive deserialization with explicit defaults

Build all integration code to handle missing or null fields explicitly:
JavaScript
function parseRecurringGift(raw) {
  return {
    id: raw.id,
    amount: raw.amount ?? 0,
    currency: raw.currency ?? 'USD',
    frequency: raw.frequency ?? 12,
    status: raw.status ?? null,
    hasPaymentFailed: raw.hasPaymentFailed ?? false,
    successfulCycles: raw.successfulCycles ?? 0,
    expMonthAndYear: raw.expMonthAndYear ?? null,
    paymentInfo: raw.paymentInfo ?? null,
    projectAllocation: raw.projectAllocation ?? [],
    donor: raw.donor ?? null,
    crmKey: raw.crmKey ?? null,
  };
}
A parse function like this:
  • Documents which fields the integration uses (everything else is ignored).
  • Handles missing fields with sensible defaults.
  • Provides a single place to update when the platform changes the shape.
  • Catches drift early — if a field that was supposed to be present is missing, the default surfaces rather than crashing.

Monitoring for drift

The defensive patterns handle most changes invisibly. For changes that need attention, monitoring catches them before they cause customer-facing issues.

Track unexpected response shapes

Log fields the integration didn’t expect:
JavaScript
function parseGiftWithDriftDetection(raw) {
  const KNOWN_FIELDS = new Set([
    'id', 'amount', 'date', 'donorId', 'donor', 'campaignName',
    'segment', 'projects', 'status', 'modifiedDate',
    /* ... etc ... */
  ]);

  const unknownFields = Object.keys(raw).filter((k) => !KNOWN_FIELDS.has(k));
  if (unknownFields.length > 0) {
    driftMetrics.increment('unknown_fields', { resource: 'gift', fields: unknownFields });
  }

  return parseGift(raw);
}
When the metric shows new fields appearing, the integration team can decide whether to start using them.

Track unknown enum values

Same pattern for integer enums:
JavaScript
function formatFrequency(integer) {
  if (!(integer in KNOWN_FREQUENCIES)) {
    driftMetrics.increment('unknown_enum', { type: 'frequency', value: integer });
  }
  return KNOWN_FREQUENCIES[integer] ?? `Frequency ${integer}`;
}
An alert on this metric catches new enum values as they appear in production.

Track unexpected error responses

If the integration suddenly starts seeing 400 errors it didn’t see before, that may indicate the API has tightened validation:
JavaScript
if (response.status === 400 && !isExpected400(problem)) {
  driftMetrics.increment('unexpected_400', { endpoint, problem_type: problem.title });
}
Sustained spikes in unexpected 400s may mean a payload your integration sends has become invalid — investigate and adapt.

Track null where you expected a value

If a field that’s typically populated starts coming back null frequently, something has changed:
JavaScript
const giftsWithoutDonor = recentGifts.filter((g) => !g.donor?.email).length;
if (giftsWithoutDonor > recentGifts.length * 0.1) {
  driftMetrics.increment('high_null_rate', { field: 'donor.email' });
}
A 10% rate of missing donor emails is anomalous; an alert catches it for investigation.

When breaking changes do happen

Even with the best intentions, breaking changes occasionally happen — a field is renamed, an endpoint is restructured, an enum value is repurposed. The platform team announces these in advance (typically through release notes, partner email, or admin UI banners). When you see one:
1

Read the announcement carefully

Identify exactly what changes, when it takes effect, and whether there’s a compatibility window.
2

Inventory your integration's exposure

Which code paths touch the affected resource or field? Search the codebase for the field name, endpoint path, or enum integer.
3

Build the new behavior alongside the old

Don’t tear out the old code until the new code is tested and verified working.
4

Test against a staging or developer organization first

If the platform offers a way to opt into the new behavior early, use it.
5

Roll out gradually

For multi-customer integrations, roll out to a few customers first, monitor, then expand.
6

Remove the old code path after the change is universal

Don’t leave dead code lying around — it accumulates and obscures the working code.
The defensive patterns on this page reduce the frequency and impact of these moments, but they don’t eliminate them entirely.

A potential future: API versioning

The unversioned namespace is the current state, but it’s possible the platform will introduce explicit versioning at some point. Speculation about what that might look like:
Possible futureImplication
/v2/api/... prefix introducedIntegrations would need to switch over; both could exist in parallel for a window
Header-based versioning (Api-Version: 2)Less URL churn; integrations send the version they expect
Both old and new shapes returned in transitionMaximally compatible; old clients work unchanged
Per-endpoint versioning (/api/v2/Donor/list)Allows incremental migration
Until such a system exists, the patterns on this page are how integrations handle change. If versioning is introduced, the defensive patterns still apply — they just become less critical for backward-compatible changes within a version.

What /api/ (with no version) actually buys

Despite the lack of explicit versioning, the unversioned namespace has some practical advantages:
AdvantageDescription
No version-migration treadmillIntegrations don’t need to plan migrations to keep up with version deprecations
Additive changes are freeNew fields and endpoints don’t break existing integrations
Platform team is incentivized to be careful with breaking changesThe pain of breaking everyone at once means breaking changes are rare
Simpler URL patternsNo mental overhead of “what version am I on?”
The cost is: when breaking changes do happen, they’re more disruptive. The defensive patterns on this page absorb most of that disruption.

A durability checklist

Walk through this when designing the integration:
  • JSON parsing tolerates fields the integration doesn’t recognize
  • Optional-chaining used everywhere for nested field access
  • All field reads use ?? defaultValue for explicit defaults
  • Unknown enum values logged but don’t crash
  • Feature detection used instead of version detection where features vary
  • Integration tests run against live API or recent fixtures, not just unit-test mocks
  • Drift metrics track unknown fields, unknown enums, unexpected 400s, unexpected null rates
  • Alerts on drift-metric spikes
  • Mappings between integer enums and labels are cached, not hardcoded as if eternal
  • Reference data lookups handle the “what if this Project no longer exists?” case
  • Webhook payload shape isn’t assumed to be exhaustive — new fields can appear
  • Documentation references include the date the integration was built against, so divergence over time is visible
  • A regular cadence exists for reviewing the spec for changes (quarterly at minimum)
The investment in defensive coding upfront pays dividends every time the platform evolves. The cost of not investing pays its toll the first time an unexpected change breaks production.

A final note: durability over cleverness

The single biggest factor in long-term integration health is resisting the temptation to be clever about the platform’s current shape. A “minimal” integration that hardcodes assumptions (“the response always has exactly these 12 fields”, “frequency is always one of these 7 values”, “this enum integer means X forever”) will break repeatedly as the platform evolves. A “defensive” integration that handles missing fields, unknown enums, and unexpected shapes will keep working through years of change. The defensive version is sometimes more verbose. It’s almost always worth the verbosity.

Where to go next

Statuses and Lifecycle States

The integer-enum landscape the defensive patterns on this page apply to.

Security and Credential Management

The complementary durability story for credentials.

Error Recovery Patterns

The error-handling patterns that pair with versioning durability.

The Raise Data Model

The current data model — defensive coding makes the integration durable as this evolves.
Last modified on May 21, 2026