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:
- You send a purchase request
- The request times out (but the server received it)
- You retry the request
- 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.
{
"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:
| 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 UTCA1B2C3→ 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
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
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
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
def generate_external_reference
timestamp = Time.now.to_i.to_s
suffix = SecureRandom.alphanumeric(6)
"#{timestamp}#{suffix}"
end
# => "1736234400A1B2C3"
Example Implementation
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:
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
| 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
new Date(1736234400 * 1000).toISOString();
// => "2025-01-07T07:00:00.000Z"
Querying by Reference
You can find a transaction by its external reference:
curl -X GET \
-H "Authorization: Bearer sk_live_your_secret_key" \
"https://my.rizpay.app/api/partners/v1/account/transactions?reference=1736234400A1B2C3"
Best Practices
- Always use external_reference - Even if you think you don't need it
- Store the reference - Save it with your order for reconciliation
- Generate once, retry with same - Use the same reference for retries
- Handle DUPLICATE_REFERENCE gracefully - It means the order was processed
- Use current timestamp - The timestamp must be from the same day
Next Steps
- Error Handling - Handle duplicate reference errors
- Webhooks - Get notified of transaction status
