NEW APP AVAILABLE FOR DOWNLOAD NOW

Get it on Google PlayDownload on the App Store

Duplicate Prevention

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_mtn_airtime",
  "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:

ComponentLengthFormatDescription
Timestamp10 digitsUnix timestampSeconds since Jan 1, 1970 UTC
Suffix6 charactersAlphanumericUnique identifier (A-Z, a-z, 0-9)
Total16 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.status.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.status.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

RuleDescription
LengthExactly 16 characters
FormatFirst 10 digits (timestamp) + 6 alphanumeric
TimestampMust be within the same day as the transaction
UniquenessMust be unique per account
ReuseCannot reuse, even for failed transactions

Error Responses

Error CodeMessageCause
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