Skip to main content
Payment failures are a normal part of running a donation platform. Cards expire, accounts close, balances run low, gateways have transient issues. Partner integrations that handle failures well — distinguishing transient errors from permanent ones, recovering recurring schedules promptly, and surfacing the right signals to the customer’s stewardship team — are much more useful than integrations that simply log errors and move on. This workflow covers the three classes of payment failures in Raise: one-time donation failures (from POST /api/Raise/give), recurring schedule failures (the hasPaymentFailed flag), and the recovery flows that bring failed schedules back to active.

Three classes of failure

ClassWhen it happensHow to detectRecovery path
One-time donation failureA POST /api/Raise/give request fails at the gateway400 Bad Request with payment-specific detail in the ProblemDetails bodySurface to donor; let them try a different payment method
Recurring schedule failureA scheduled charge cycle failshasPaymentFailed: true on the RecurringGift recordDonor outreach to update payment method
Gateway-level outageThe payment gateway is temporarily unavailable5xx responses, network errors, or batch failures across multiple submissionsRetry with backoff; escalate if sustained
Each class needs a different response. The next sections walk through them in detail.

One-time donation failures

When a donor submits a donation through POST /api/Raise/give and the gateway declines the charge, the request returns 400 Bad Request with a ProblemDetails body. The Gift record is not created in this case.

Classifying the failure

The most useful field is detail in the response body. Common payment-failure reasons:
Detail (representative)CauseRecoverable?
"Card declined"Gateway declined the charge (insufficient funds, fraud flag, etc.)No — same card will fail again; donor needs a different method.
"Card expired"The card’s expiration date has passedNo — donor must use a different card.
"Invalid card number"Tokenization or transcription errorNo — donor must re-enter card details.
"Insufficient funds"Account balance below the donation amountSometimes — donor can try again after funding the account.
"Issuer unavailable"The card issuer’s authorization system is temporarily unreachableYes — retry after a short delay (minutes).
"Gateway timeout"The payment gateway didn’t respond in timeYes — retry with caution (could be a successful charge that didn’t complete the API response).
The exact detail text returned by Raise depends on the payment gateway configured for the customer’s organization. Gateway-specific messages may vary (Stripe says one thing, Authorize.net says another). Don’t switch on exact detail strings — they’re not a stable contract. Use them for logging and user-facing surfacing, not for retry logic.⚠️ Spec gap: The OpenAPI spec doesn’t enumerate the standard payment-failure messages Raise produces. The list above is representative; the canonical mapping isn’t published. Coordinate with the platform team for the authoritative list if your integration’s classification logic needs it.

Handling a one-time failure

JavaScript
async function processDonation(donationRequest) {
  const response = await fetch(
    'https://prod-api.raisedonors.com/api/Raise/give',
    {
      method: 'POST',
      headers: {
        Authorization: `Bearer ${process.env.RAISE_API_TOKEN}`,
        'Content-Type': 'application/json',
      },
      body: JSON.stringify(donationRequest),
    }
  );

  if (response.ok) {
    return { success: true, gift: await response.json() };
  }

  const problem = await response.json();

  // Classify the failure
  const classification = classifyPaymentFailure(response.status, problem);

  return {
    success: false,
    classification,
    detail: problem.detail,
    fieldErrors: problem.errors,
  };
}

function classifyPaymentFailure(status, problem) {
  if (status === 401) return 'auth_error';
  if (status === 429) return 'rate_limited';
  if (status >= 500) return 'gateway_error';

  // 400 — could be validation or payment failure
  if (problem.errors) return 'validation_error';

  // Payment-failure 400: surface to donor
  return 'payment_declined';
}
The right response by classification:
ClassificationResponse
validation_errorFix the request body and re-submit. Surface field-level errors to the UI.
payment_declinedDisplay a clear “your payment was declined — please try a different method” message. Don’t retry the same card.
auth_errorDon’t retry — the API token needs to be replaced. Alert the integration owner.
rate_limitedHonor Retry-After and back off. See Rate Limits.
gateway_errorRetry with caution — see “Idempotency considerations” in Process a Donation.

What the donor sees

For donor-facing flows (forms embedded on a customer’s site), the partner integration’s job is to display a clear, actionable error message. A few patterns:
Failure typeDonor-facing message
Card declined”Your card was declined. Please try a different payment method.”
Card expired”Your card has expired. Please enter a card with a valid expiration date.”
Invalid card”We couldn’t process this card. Please check the card number and try again.”
Network/gateway error”We’re having trouble processing your donation right now. Please try again in a few minutes.”
Generic fallback”Your donation couldn’t be processed. Please try again or contact support.”
Don’t surface the raw detail string to donors — it may contain technical jargon, gateway-specific codes, or unhelpful messages. Map to a friendly message and log the raw detail server-side for diagnostics.

Recurring schedule failures

A failed payment on a recurring schedule produces a different signal: the schedule’s hasPaymentFailed flag becomes true. The schedule remains in the system but stops processing new charge cycles until the donor’s payment method is updated.

Detecting failed schedules

Query for failed schedules on a regular cadence — typically daily as part of the customer’s stewardship workflow:
JavaScript
async function findFailedSchedules() {
  const allFailedSchedules = [];
  let skip = 0;
  const take = 1000;

  while (true) {
    const response = await fetch(
      'https://prod-api.raisedonors.com/api/RecurringGift/query',
      {
        method: 'POST',
        headers: {
          Authorization: `Bearer ${process.env.RAISE_API_TOKEN}`,
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({
          skip,
          take,
          sortBy: 'id',
          groups: [
            {
              conditions: [
                {
                  parameter: 'hasPaymentFailed',
                  operator: 0 /* equals — discover via QueryOptions */,
                  value: 'true',
                },
              ],
            },
          ],
        }),
      }
    );
    const page = await response.json();
    allFailedSchedules.push(...page.items);

    if (page.items.length < take) break;
    skip += take;
  }

  return allFailedSchedules;
}
For ongoing monitoring, store the last-known set of failed schedule IDs and compare to the latest query result on each cycle. New entries (schedules that flipped from healthy to failed since the last check) are the highest-priority candidates for donor outreach.

Inspecting payment history

To understand why a specific schedule failed, fetch its activity history:
cURL
curl https://prod-api.raisedonors.com/api/RecurringGift/9876/activities \
  -H "Authorization: Bearer YOUR_API_TOKEN"
The activity records typically include each cycle’s outcome (success or failure) and the reason. Useful for showing the customer’s stewardship team a clear picture before they contact the donor.

The recovery flow

The typical flow for recovering a failed schedule:
1

Detect the failure

Daily failed-schedules query identifies the schedule. New failures (not previously known) flow into the recovery queue.
2

Generate a personalized donor page

Use POST /api/Donor/{donorId}/generate-page to create a URL pre-filled with the donor’s identity. The donor will land on a page where they can update payment info and resume their schedule.
3

Send the outreach email

Email the donor with the personalized URL. Be clear and kind: explain that their support has paused, that updating their payment method takes a few seconds, and provide the direct link.
JavaScript
async function sendPaymentUpdateEmail(donor, scheduleAmount) {
  const pageResp = await fetch(
    `https://prod-api.raisedonors.com/api/Donor/${donor.id}/generate-page`,
    {
      method: 'POST',
      headers: {
        Authorization: `Bearer ${process.env.RAISE_API_TOKEN}`,
        'Content-Type': 'application/json',
      },
    }
  );
  const { pageUrl } = await pageResp.json();

  await emailService.send({
    to: donor.email,
    subject: 'Update your payment method to continue your support',
    body: `Hi ${donor.firstName},

We weren't able to process your most recent ${scheduleAmount} donation. To keep your support going, please take a moment to update your payment method:

${pageUrl}

Thank you for your generosity.`,
  });
}
4

Track the outreach

Record that you’ve contacted the donor and when. On the next daily run, skip donors you’ve already notified within the past N days to avoid spamming.
5

Re-check the schedule

After the donor updates their payment method (typically through the personalized page), the next charge cycle will succeed and hasPaymentFailed flips back to false. Your daily query naturally stops returning the schedule.
6

Escalate persistent failures

Schedules that remain hasPaymentFailed: true after multiple outreach attempts (e.g., 30 days, 3 emails) should be escalated for human review by the customer’s stewardship team. They may need phone outreach or eventual schedule cancellation.

Proactive: detect upcoming card expirations

A more effective pattern: contact donors before their card actually fails. Check the expMonthAndYear field on active schedules:
JavaScript
async function findSchedulesExpiringWithinMonths(monthsAhead) {
  const allActive = await listAllActiveRecurringGifts();
  const now = new Date();
  const cutoff = new Date(now.getFullYear(), now.getMonth() + monthsAhead, 1);

  return allActive.filter((schedule) => {
    if (!schedule.expMonthAndYear) return false;
    const [month, year] = schedule.expMonthAndYear.split('/').map(Number);
    const expDate = new Date(year, month - 1, 1);
    return expDate < cutoff && expDate > now;
  });
}
The exact format of expMonthAndYear (MM/YYYY, MM/YY, or another shape) is not documented in the spec. Confirm against live data before parsing in production. See Recurring Gifts: Detecting upcoming card expirations.
Donors notified before their cards expire respond at much higher rates than donors notified after the failure has already happened. The proactive flow is the same as the recovery flow — generate a personalized page, send an email — but with friendlier framing (“just a heads up, your card on file expires next month”).

Gateway-level outages

The third class of failures: the payment gateway itself is temporarily unavailable. Symptoms include:
  • Multiple POST /api/Raise/give requests across different customers returning 5xx responses or timing out within a short window.
  • Multiple recurring schedules flipping to hasPaymentFailed: true in close succession.
  • Network errors connecting to the API host.
For partner integrations:

What to do during a likely outage

BehaviorDescription
Slow down submissionsDon’t hammer a struggling system with retries. Back off aggressively.
Queue retries for laterFor non-time-sensitive donations, queue and retry after the outage resolves.
Alert on sustained failuresIf >20% of recent submissions fail over 10+ minutes, alert the integration owner — this likely indicates an upstream issue, not random failures.
Surface to donors clearlyReal-time donation flows should show a clear “we’re having trouble — please try again in a few minutes” message rather than appearing broken.

What not to do

Anti-patternWhy
Retry foreverA 5xx that persists for hours is a gateway outage, not a transient blip. Cap retries and surface for human review.
Retry without Retry-After awarenessIf Raise returns 429 Too Many Requests, honor the header. Adding more retries during throttling makes things worse.
Silently absorb failuresFailed donations are lost donations. Make sure the failure is logged, surfaced, and (where appropriate) queued for retry.
See Error Recovery Patterns for the broader retry-with-circuit-breaker pattern that handles outages gracefully.

Webhook events for payment failures

The Raise webhook system delivers events when records change. For payment failures, the relevant events are typically:
Likely eventWhat it signals
RecurringGift update eventsThe schedule’s hasPaymentFailed flag changed — useful for real-time detection without polling.
Gift eventsA new Gift was created (successful charge) or a Gift was refunded.
⚠️ Spec gap: The Raise OpenAPI spec doesn’t label the EventType enum integers ([10, 11, 12, 20, 21, 22, 30, 31, 32, 40, 41, 42, 50, 51, 52]). Identifying the specific event type integers that correspond to payment-failure scenarios requires discovery against the live API or coordination with the platform team.Until the labels are published, the daily-polling pattern shown above is the most reliable detection method. When event type labels become available, the same recovery flow can be triggered in real time from webhook events instead of from the polling query.
The webhook log endpoints (GET /api/Webhook/{id}/log/list, GET /api/Webhook/{id}/log/{logId}) are particularly useful here — partner integrations can confirm which events have fired for a customer and inspect the payload shapes to map event type integers to their semantic meanings.

A complete failed-payment monitor

Pulling the pieces together as a daily monitor job:
JavaScript
class FailedPaymentMonitor {
  constructor({ token, emailService, outreachTracker }) {
    this.token = token;
    this.emailService = emailService;
    this.outreachTracker = outreachTracker;
  }

  async runDaily() {
    const failedSchedules = await this.findFailedSchedules();
    const expiringSchedules = await this.findSchedulesExpiringWithinMonths(2);

    const newFailures = [];
    const newExpiringSoon = [];

    for (const schedule of failedSchedules) {
      if (!await this.outreachTracker.hasContacted(schedule.id, 'failed', 14)) {
        newFailures.push(schedule);
      }
    }

    for (const schedule of expiringSchedules) {
      if (!await this.outreachTracker.hasContacted(schedule.id, 'expiring', 30)) {
        newExpiringSoon.push(schedule);
      }
    }

    for (const schedule of newFailures) {
      await this.notifyFailure(schedule);
    }
    for (const schedule of newExpiringSoon) {
      await this.notifyExpiringSoon(schedule);
    }

    return {
      failedCount: newFailures.length,
      expiringCount: newExpiringSoon.length,
    };
  }

  async notifyFailure(schedule) {
    const pageUrl = await this.generateDonorPage(schedule.donor.id);
    await this.emailService.send({
      to: schedule.donor.email,
      template: 'payment-failed',
      data: { donor: schedule.donor, pageUrl, amount: schedule.formattedAmount },
    });
    await this.outreachTracker.recordContact(schedule.id, 'failed');
  }

  async notifyExpiringSoon(schedule) {
    const pageUrl = await this.generateDonorPage(schedule.donor.id);
    await this.emailService.send({
      to: schedule.donor.email,
      template: 'card-expiring',
      data: { donor: schedule.donor, pageUrl, expDate: schedule.expMonthAndYear },
    });
    await this.outreachTracker.recordContact(schedule.id, 'expiring');
  }

  // findFailedSchedules, findSchedulesExpiringWithinMonths, generateDonorPage
  // omitted for brevity — see the snippets earlier on this page.
}
Run this on a daily cron. Track outreach in a small database to avoid duplicate notifications. Monitor the counts over time — sudden spikes in failedCount may indicate a gateway issue rather than typical donor activity.

Where to go next

Configure a Recurring Gift

The full recurring schedule lifecycle including the failure detection patterns.

Process a Donation

The end-to-end donation flow including the one-time failure handling.

Error Recovery Patterns

The general retry, circuit-breaker, and dead-letter patterns that apply to payment failures.

Webhooks Overview

Subscribe to payment events for real-time failure detection.
Last modified on May 20, 2026