---
title: Duplicate Prevention
description: Prevent duplicate transactions using external references
---

Network issues and retries can lead to duplicate transactions. Learn how to prevent them using external references.

## The Problem

Consider this scenario:

1. You send a purchase request
2. The request times out (but the server received it)
3. You retry the request
4. Now there are two purchases for the same order

This can result in:

- Double charges to customers
- Inventory discrepancies
- Financial reconciliation issues

## The Solution: External References

Every purchase request accepts an `external_reference` parameter. This is your unique identifier for the transaction.

```json
{
  "product_id": "prd_42",
  "phone_number": "08012345678",
  "amount": "500.00",
  "external_reference": "1736234400A1B2C3"
}
```

If you send the same `external_reference` twice, the second request returns a `DUPLICATE_REFERENCE` error instead of creating a duplicate transaction.

## Reference Format

The `external_reference` must follow a specific format:

| Component | Length            | Format         | Description                       |
| --------- | ----------------- | -------------- | --------------------------------- |
| Timestamp | 10 digits         | Unix timestamp | Seconds since Jan 1, 1970 UTC     |
| Suffix    | 6 characters      | Alphanumeric   | Unique identifier (A-Z, a-z, 0-9) |
| **Total** | **16 characters** |                |                                   |

**Regex**: `/^\d{10}[a-zA-Z0-9]{6}$/`

**Example**: `1736234400A1B2C3`

- `1736234400` → Jan 7, 2025 07:00:00 UTC
- `A1B2C3` → Unique suffix

### Why This Format?

- **Timestamp prefix**: Natural ordering, easy debugging, audit trail
- **Unix format**: Timezone-agnostic, universal standard, compact
- **Alphanumeric suffix**: Prevents collisions for multiple transactions per second
- **Fixed length**: Consistent, easy to validate

## Code Examples

### JavaScript / Node.js

```javascript
function generateExternalReference() {
  const timestamp = Math.floor(Date.now() / 1000);
  const chars =
    "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
  let suffix = "";
  for (let i = 0; i < 6; i++) {
    suffix += chars.charAt(Math.floor(Math.random() * chars.length));
  }
  return `${timestamp}${suffix}`;
}
// => "1736234400A1B2C3"
```

### Python

```python
import time
import random
import string

def generate_external_reference():
    timestamp = str(int(time.time()))
    suffix = ''.join(random.choices(string.ascii_letters + string.digits, k=6))
    return f"{timestamp}{suffix}"
# => "1736234400A1B2C3"
```

### PHP

```php
function generateExternalReference(): string {
    $timestamp = time();
    $chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
    $suffix = '';
    for ($i = 0; $i < 6; $i++) {
        $suffix .= $chars[random_int(0, strlen($chars) - 1)];
    }
    return $timestamp . $suffix;
}
// => "1736234400A1B2C3"
```

### Ruby

```ruby
def generate_external_reference
  timestamp = Time.now.to_i.to_s
  suffix = SecureRandom.alphanumeric(6)
  "#{timestamp}#{suffix}"
end
# => "1736234400A1B2C3"
```

## Example Implementation

```javascript
async function purchaseAirtime(order) {
  // Generate a unique reference
  const reference = generateExternalReference();

  try {
    const response = await fetch(
      "https://my.rizpay.app/api/partners/v1/purchases",
      {
        method: "POST",
        headers: {
          Authorization: `Bearer ${API_KEY}`,
          "Content-Type": "application/json",
        },
        body: JSON.stringify({
          product_id: order.productId,
          phone_number: order.phoneNumber,
          amount: order.amount,
          external_reference: reference,
        }),
      }
    );

    const data = await response.json();

    if (data.error?.code === "DUPLICATE_REFERENCE") {
      // This reference was already used
      // Fetch the existing transaction instead
      return await getTransactionByReference(reference);
    }

    return data.data;
  } catch (error) {
    // Safe to retry with the SAME reference - prevents duplicates
    throw error;
  }
}
```

## Safe Retry Pattern

With external references, retries are safe:

```javascript
async function purchaseWithRetry(order, maxRetries = 3) {
  // Generate reference ONCE, reuse for retries
  const reference = generateExternalReference();

  for (let attempt = 0; attempt < maxRetries; attempt++) {
    try {
      const response = await makePurchase(order, reference);

      if (response.error?.code === "DUPLICATE_REFERENCE") {
        // Already processed - this is success, not an error
        console.log("Order already processed");
        return { success: true, alreadyProcessed: true };
      }

      return { success: true, data: response.data };
    } catch (error) {
      if (attempt === maxRetries - 1) {
        throw error;
      }
      // Wait before retry
      await new Promise((r) => setTimeout(r, 1000 * (attempt + 1)));
    }
  }
}
```

## Validation Rules

| Rule       | Description                                    |
| ---------- | ---------------------------------------------- |
| Length     | Exactly 16 characters                          |
| Format     | First 10 digits (timestamp) + 6 alphanumeric   |
| Timestamp  | Must be within the same day as the transaction |
| Uniqueness | Must be unique per account                     |
| Reuse      | Cannot reuse, even for failed transactions     |

## Error Responses

| Error Code            | Message                                            | Cause                 |
| --------------------- | -------------------------------------------------- | --------------------- |
| `VALIDATION_ERROR`    | "External reference must be exactly 16 characters" | Wrong length          |
| `VALIDATION_ERROR`    | "External reference format invalid"                | Doesn't match pattern |
| `VALIDATION_ERROR`    | "External reference timestamp must be same day"    | Timestamp too old     |
| `DUPLICATE_REFERENCE` | "External reference already used"                  | Reference reused      |

## Converting Timestamp Back to Date

The Unix timestamp can be converted back for debugging:

```javascript
// JavaScript
new Date(1736234400 * 1000).toISOString();
// => "2025-01-07T07:00:00.000Z"
```

## Querying by Reference

You can find a transaction by its external reference:

```bash
curl -X GET \
  -H "Authorization: Bearer sk_live_your_secret_key" \
  "https://my.rizpay.app/api/partners/v1/account/transactions?reference=1736234400A1B2C3"
```

## Best Practices

1. **Always use external_reference** - Even if you think you don't need it
2. **Store the reference** - Save it with your order for reconciliation
3. **Generate once, retry with same** - Use the same reference for retries
4. **Handle DUPLICATE_REFERENCE gracefully** - It means the order was processed
5. **Use current timestamp** - The timestamp must be from the same day

## Next Steps

- [Error Handling](/docs/getting-started/errors) - Handle duplicate reference errors
- [Webhooks](/docs/webhooks/overview) - Get notified of transaction status