Reward redemption via API fails with 'Duplicate Request' error in loyalty programs module

Our customer-facing app allows users to redeem loyalty rewards through the Zendesk Sell API. Recently, we’ve been getting ‘Duplicate Request’ errors even for first-time redemption attempts. Customers are unable to redeem their rewards, and retrying the request continues to fail with the same error.

The reward redemption endpoint is rejecting requests with a 409 conflict status. We’re not intentionally sending duplicate requests - each redemption attempt has a unique timestamp and customer context. The error message suggests using an idempotency key, but the API documentation for zs-2022 doesn’t clearly explain how to implement this.

Error response:

{"error": "duplicate_request",
 "message": "This redemption has already been processed",
 "idempotency_key_required": true}

How should we properly handle idempotency for reward redemptions to prevent these false duplicate errors?

I can provide detailed guidance on implementing idempotency for reward redemptions in zs-2022. This involves three critical aspects:

1. Idempotency Key Usage

The reward redemption endpoint requires an idempotency key to prevent duplicate processing. Include this header with every redemption request:


Idempotency-Key: {unique_key_value}
POST /api/v2/loyalty/rewards/redeem

Key Generation Strategy: Generate keys that are:

  • Unique per redemption intent: Different redemptions must have different keys
  • Consistent for retries: Same redemption retried should use same key
  • Cryptographically random: Use UUID v4 or secure random generation

Recommended approach:


// Pseudocode - Key generation:
1. Generate UUID when user initiates redemption
2. Store UUID with redemption intent (database/cache)
3. Use same UUID for all retry attempts of that redemption
4. Generate new UUID only for new redemption intents
5. Include: redemption_id + customer_id + timestamp_hash

Example implementation:

  • User clicks ‘Redeem’ → Generate UUID: `a1b2c3d4-e5f6-7890-abcd-ef1234567890
  • Store in session/database with redemption details
  • Send request with header: `Idempotency-Key: a1b2c3d4-e5f6-7890-abcd-ef1234567890
  • If request fails → Retry with SAME key
  • New redemption → Generate NEW UUID

2. Reward Redemption Endpoint Requirements

The redemption endpoint has specific requirements:

Required Headers:


Content-Type: application/json
Authorization: Bearer {token}
Idempotency-Key: {uuid}

Request Payload:

{
  "customer_id": "cust_12345",
  "reward_id": "reward_67890",
  "points_amount": 500,
  "redemption_type": "discount"
}

Endpoint Behavior:

  • Without idempotency key → 409 Duplicate Request error (as you’re seeing)
  • With valid key (first attempt) → 200 Success, processes redemption
  • With same key (retry) → 200 Success, returns original redemption result (doesn’t reprocess)
  • With same key after 24 hours → 409 Conflict, key expired

3. Duplicate Request Handling

Client-Side Prevention:

  • Disable UI controls after redemption initiated
  • Show loading state during API call
  • Generate idempotency key before request
  • Store key with redemption intent for retry scenarios

Server-Side Handling: Implement proper retry logic:


// Pseudocode - Retry with idempotency:
1. Generate and store idempotency key
2. Attempt redemption request with key
3. If network error or 5xx response:
   - Retry with SAME idempotency key
   - Use exponential backoff (1s, 2s, 4s)
   - Max 3 retry attempts
4. If 409 Duplicate Request:
   - Check if key matches stored key
   - If yes: Likely race condition, fetch redemption status
   - If no: Generate new key and retry
5. If 200 Success: Mark redemption complete

Race Condition Handling: If you receive 409 even with proper idempotency key:

  • Query redemption status: `GET /api/v2/loyalty/redemptions/{customer_id}
  • Check if redemption already completed
  • Verify points balance to confirm processing

Idempotency Key Management:

Storage Strategy:

  • Store keys in database with redemption records
  • Include: key, customer_id, reward_id, timestamp, status
  • TTL: 24 hours (matches API idempotency window)
  • Index by key for fast duplicate detection

Key Lifecycle:

  1. Generation: When user initiates redemption
  2. Active: During API call and retries (same key)
  3. Completed: After successful redemption (mark as processed)
  4. Expired: After 24 hours (can be reused)

Debugging Your Current Issue:

Since you’re getting false duplicates without idempotency keys, the API is likely using fallback duplicate detection based on:

  • Customer ID + Reward ID + Timestamp proximity
  • Request payload hash
  • Session identifiers

This can trigger false positives if:

  • Multiple requests sent within seconds
  • Network retries without proper idempotency
  • Load balancer duplicating requests

Immediate Fix:

  1. Add idempotency key header to all redemption requests
  2. Generate UUID v4 for each new redemption intent
  3. Store key with redemption record for retry consistency
  4. Implement client-side duplicate prevention (disable button)
  5. Add retry logic with exponential backoff using same key

Example Implementation:


POST /api/v2/loyalty/rewards/redeem
Idempotency-Key: f47ac10b-58cc-4372-a567-0e02b2c3d479

{
  "customer_id": "cust_12345",
  "reward_id": "reward_67890",
  "points_amount": 500
}

With proper idempotency implementation, the API will correctly distinguish between:

  • New redemptions (different keys) → Process normally
  • Retries (same key) → Return original result without reprocessing
  • Duplicates (different keys, same redemption) → Detect and prevent

This ensures customers can redeem rewards reliably even with network issues or UI race conditions.

Make sure you understand the idempotency window. Most APIs only honor idempotency keys for a limited time (usually 24 hours). After that window, the same key is treated as a new request. This is important for your key generation strategy - you don’t want keys that could accidentally collide across the idempotency window. Include date/time components in your key generation logic.

For idempotency keys, use a UUID or hash that’s consistent for the same redemption but unique across different redemptions. For example, hash the combination of customer_id, reward_id, and redemption_timestamp. If the request fails and you retry, use the same idempotency key so the API knows it’s a retry, not a duplicate redemption.

The key should be stored on your side so you can use the same value for retries of the same operation.

Check if there’s network-level duplication happening. If your app is behind a load balancer or using retry middleware, you might be sending duplicate requests without realizing it. The API is correctly detecting duplicates, but they’re coming from your infrastructure layer rather than your application code. Enable request logging to see if multiple requests are actually being sent.

The idempotency key is a header you need to include with your request. Use something like ‘X-Idempotency-Key’ or ‘Idempotency-Key’ header with a unique value for each logical redemption attempt. This allows the API to distinguish between retries of the same operation and genuinely new requests.

We had this issue in our mobile app. Users were tapping the redeem button multiple times because the API response was slow, creating race conditions. We fixed it by disabling the button after first tap and generating an idempotency key client-side before making the request. The key persists through app restarts so genuine retries after failures use the same key.