Loyalty API points redemption fails with 'Insufficient Balance' despite correct balance

We’re integrating Salesforce Loyalty Management with our e-commerce platform using the Loyalty API for real-time points redemption at checkout. Customers are experiencing failed redemptions with ‘Insufficient Balance’ errors even though their loyalty dashboard shows adequate points.

API error response:


Error: INSUFFICIENT_LOYALTY_BALANCE
Requested: 500 points
Available: 450 points
Member ID: LM-00123456

But when we check the member’s balance in Salesforce, it shows 850 points. The discrepancy appears to be related to recent point accruals that haven’t synchronized to the API’s balance calculation. We’re seeing a 5-10 minute delay between points being awarded and becoming available for redemption.

Our redemption flow calls GET /loyalty/members/{id}/balance followed by POST /loyalty/redemptions if sufficient balance exists. The balance check passes, but the redemption fails, causing customer frustration at checkout. This is particularly problematic during promotional periods when customers earn and immediately try to redeem bonus points.

How do we ensure real-time balance synchronization in Loyalty Management API integration? We’re on Winter '25 and this is affecting customer satisfaction significantly.

Your redemption failures stem from three interconnected issues in Loyalty Management API integration: transaction processing timing, balance calculation methodology, and API call sequencing. Here’s the complete solution:

1. Loyalty Management API Integration - Configure Real-Time Processing:

The 5-10 minute delay indicates batch processing mode. Reconfigure your Loyalty Program for real-time transaction processing:

In Salesforce Setup → Loyalty Management Settings → Loyalty Programs:

  • Select your program
  • Set Processing Mode: Real-time (not Batch)
  • Set Transaction Processing: Immediate
  • Enable Real-time Balance Updates: true

For API-driven point accruals, always include the immediate processing flag:

POST /loyalty/transactions
{
  "memberId": "LM-00123456",
  "points": 100,
  "processImmediately": true,
  "journalType": "Accrual"
}

This ensures points are immediately available for redemption rather than queued for batch processing.

2. Real-Time Balance Synchronization - Implement Atomic Operations:

Your current two-step process (check balance, then redeem) creates a race condition. Replace with atomic redemption:

Current Problematic Pattern:

// Step 1: Check balance
const balance = await GET('/loyalty/members/{id}/balance');
if (balance.available >= requestedPoints) {
  // Step 2: Redeem (might fail if balance changed)
  await POST('/loyalty/redemptions', {points: requestedPoints});
}

Correct Atomic Pattern:

// Single atomic operation
const redemption = await POST('/loyalty/redemptions', {
  memberId: 'LM-00123456',
  points: 500,
  validateBalance: true,
  failOnInsufficientBalance: true
});

The Loyalty API handles balance validation within the transaction, eliminating race conditions.

3. Redemption Error Handling - Distinguish Balance Types:

Loyalty Management tracks multiple balance types:

  • Total Balance: All earned points
  • Available Balance: Points eligible for redemption
  • Pending Balance: Points awaiting processing
  • Reserved Balance: Points in pending redemption transactions

Your API calls must query the correct balance:

GET /loyalty/members/{id}/balance?balanceType=AVAILABLE

Not:

GET /loyalty/members/{id}/balance // Returns total, not available

Implementation Architecture:

Phase 1: Point Accrual (E-commerce → Salesforce)

// When customer completes purchase
async function awardLoyaltyPoints(orderId, customerId, points) {
  const response = await fetch('/loyalty/transactions', {
    method: 'POST',
    headers: {
      'X-Request-Id': generateIdempotencyKey(orderId),
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      memberId: customerId,
      points: points,
      transactionType: 'Purchase',
      orderId: orderId,
      processImmediately: true,
      journalSubType: 'Purchase'
    })
  });

  // Wait for processing confirmation
  await pollTransactionStatus(response.transactionId);
}

Phase 2: Balance Check (Checkout)

async function getRedemptionBalance(memberId) {

  const response = await fetch(

    `/loyalty/members/${memberId}/balance?balanceType=AVAILABLE&includeExpiring=true`,

    {

      method: 'GET',

      headers: {'Cache-Control': 'no-cache'}

    }

  );

  return {

    available: response.availableBalance,

    pending: response.pendingBalance,

    expiringSoon: response.expiringBalance,

    expiryDate: response.nextExpiryDate

  };

}

Phase 3: Redemption (Atomic)

async function redeemPoints(memberId, points, orderId) {

  try {

    const response = await fetch('/loyalty/redemptions', {

      method: 'POST',

      headers: {

        'X-Request-Id': generateIdempotencyKey(`redeem-${orderId}`),

        'Content-Type': 'application/json'

      },

      body: JSON.stringify({

        memberId: memberId,

        points: points,

        orderId: orderId,

        validateBalance: true,

        failOnInsufficientBalance: true,

        redemptionType: 'Checkout'

      })

    });

    return {success: true, redemptionId: response.id};

  } catch (error) {

    if (error.code === 'INSUFFICIENT_LOYALTY_BALANCE') {

      // Fetch current balance for error message

      const currentBalance = await getRedemptionBalance(memberId);

      return {

        success: false,

        error: 'insufficient_balance',

        requested: points,

        available: currentBalance.available,

        pending: currentBalance.pending

      };

    }

    throw error;

  }

}

Advanced Considerations:

Handling Concurrent Redemptions: If a customer attempts multiple redemptions simultaneously (e.g., multiple browser tabs):

// Use optimistic locking

POST /loyalty/redemptions

{

  "memberId": "LM-00123456",

  "points": 500,

  "lockBalance": true,  // Prevents concurrent redemptions

  "lockTimeout": 60     // Seconds

}

Transaction Reversal: Implement reversal logic for failed checkouts:

if (checkoutFailed) {

  // Reverse redemption

  POST /loyalty/transactions

  {

    "transactionType": "Reversal",

    "originalTransactionId": redemptionId,

    "processImmediately": true

  }

}

Expiring Points Awareness: Warn customers about expiring points during checkout:

if (balance.expiringBalance > 0 && balance.nextExpiryDate < 30days) {

  displayWarning(`${balance.expiringBalance} points expiring on ${balance.nextExpiryDate}`);

}

Performance Optimization:

  1. Cache Balance Strategically: Cache available balance for 30 seconds max, refresh before redemption
  2. Batch Balance Queries: If showing multiple customers’ balances, use composite API
  3. Webhook Integration: Subscribe to loyalty balance change events instead of polling
  4. Connection Pooling: Maintain persistent connections to Salesforce API

Monitoring and Alerting:

Implement comprehensive monitoring:

  • Track redemption failure rate by error type
  • Alert when pending balance exceeds 10% of total balance (indicates processing delays)
  • Monitor average time from accrual to availability
  • Track idempotency key collision rate

Customer Experience Improvements:

UI Transparency:

// Display in checkout

{

  "Total Points": 850,

  "Available Now": 450,

  "Processing": 400,

  "Can Redeem": 450

}

Graceful Degradation: If real-time balance is unavailable:

if (balanceAPIDown) {

  // Allow redemption up to last known balance

  // Flag for manual reconciliation

  processRedemptionWithFallback();

}

Testing Checklist:

  1. Test immediate redemption after point accrual
  2. Verify concurrent redemption handling
  3. Test redemption during point expiry
  4. Validate reversal scenarios
  5. Test with batch processing disabled
  6. Verify idempotency key behavior
  7. Test balance calculation with multiple transaction types

By implementing real-time processing, atomic redemption operations, and proper balance type handling, you’ll eliminate the synchronization delays and race conditions causing failed redemptions. The key is treating redemption as a single atomic transaction rather than separate check-and-redeem operations.

Implement idempotency keys in your redemption requests. If a redemption fails due to balance issues but you believe the balance is sufficient, you can safely retry with the same idempotency key without risking double redemption. The Loyalty API supports this through the X-Request-Id header.

From a customer perspective, you should also display pending points separately from available points in your UI. Make it clear which points can be redeemed immediately versus which are processing. This prevents the frustration of seeing points they can’t use.

We hit this exact issue last quarter. The problem is you’re checking balance and redeeming in two separate API calls. Between those calls, other transactions can occur or pending transactions can change status. Use the redemption API’s built-in balance check - it’s atomic. Set ‘validateBalance: true’ in your redemption request payload and handle the error response if balance is insufficient.

Good insights. I’ll check the ProcessingMode setting. But even if we fix the delay, how do we handle the race condition between balance check and redemption? Should we implement retry logic?

This is a classic eventual consistency issue. Loyalty Management uses a transaction processing model where point accruals go through a journal posting process before being available for redemption. Check if your point accrual transactions have ProcessingStatus = ‘Processed’ before attempting redemption. Pending transactions don’t count toward available balance.

The 5-10 minute delay suggests your point accruals are running through batch processing rather than real-time processing. Check your LoyaltyProgramProcess configuration. For e-commerce integrations requiring immediate redemption, you need to set ProcessingMode to ‘Realtime’ instead of ‘Batch’. Also ensure your point accrual API calls include ‘processImmediately: true’ parameter.