Skip to content
API DocsDocs

Common Integration Mistakes

Avoid these common pitfalls when integrating with the Exirom API

5 min readUpdated Mar 26, 2026

These are the most frequent mistakes developers make when integrating with Exirom. Each includes the symptom, root cause, and fix.

#1. Trusting Redirect URLs for Payment Confirmation

Symptom: Orders fulfilled for payments that actually failed.

Mistake: Using successRedirectUrl as proof of payment.

Fix: Always treat the webhook callback as the authoritative result. Redirect URLs are for user experience only — a customer can land on the success URL even if the payment ultimately fails.

See Error Recovery Patterns — Webhook vs Redirect for the full decision table and code example.

#2. Reusing requestId Across Different Orders

Symptom: New payment returns the result of an old payment.

Mistake: Using a static or poorly generated requestId.

Fix: Generate a unique requestId per order. Reuse it only when retrying the same payment.

// WRONG — same requestId for different orders
const requestId = 'my-payment';
 
// WRONG — sequential IDs are guessable
const requestId = `order-${orderCount++}`;
 
// CORRECT — UUID per order
const requestId = crypto.randomUUID();

#3. Processing Webhooks Before Responding 200

Symptom: Duplicate webhooks, duplicate order fulfillment.

Mistake: Doing database writes or external calls before sending the HTTP response.

Fix: Respond 200 immediately, then process asynchronously.

// WRONG — slow processing causes timeout and retry
app.post('/webhooks', async (req, res) => {
  await updateDatabase(req.body);      // Takes 3 seconds
  await sendConfirmationEmail(req.body); // Takes 2 seconds
  res.status(200).send('OK');           // Too late — Exirom already retried
});
 
// CORRECT — respond first, process after
app.post('/webhooks', (req, res) => {
  res.status(200).send('OK');
  processWebhookAsync(req.body); // Fire and forget
});

#4. Missing Device Data for 3DS

Symptom: Payments silently declined or stuck in CUSTOMER_VERIFICATION without a challenge URL.

Mistake: Not collecting browser/device data from the customer's device.

Fix: Collect device fields from the customer's browser and include them in the payment request.

// Required device fields for 3DS
const device = {
  ip: customerIp,                      // Server-side: req.ip or X-Forwarded-For
  userAgent: navigator.userAgent,      // Client-side
  acceptLanguage: navigator.language || 'en-US',
  javaEnabled: navigator.javaEnabled?.() ?? false,
  javaScriptEnabled: true,
  deviceLanguage: navigator.language?.split('-')[0] || 'en',
  colorDepth: String(screen.colorDepth),
  screenHeight: String(screen.height),
  screenWidth: String(screen.width),
  deviceTimezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
};

Collect these on the client side and send them to your server before initiating the payment. Server-side values (like ip) cannot be collected from the browser.

#5. Not Verifying Webhook Signatures

Symptom: Forged webhooks processed, fraudulent orders fulfilled.

Mistake: Processing all POST requests to your webhook endpoint without verifying the X-Checksum header.

Fix: Always verify the HMAC-SHA256 checksum. See Webhook Best Practices.

#6. Authenticating Per Request

Symptom: Slow API calls, hitting auth endpoint rate limits.

Mistake: Calling POST /api/v1/auth before every payment request.

Fix: Cache the token (valid for 30 days) and reuse it. See Going to Production for caching patterns. If your token expires mid-session, see Authentication Token Expired for the recovery pattern.

#7. Incorrect Checksum Amount Format

Symptom: 400 BadRequest on APM payments, checksum validation failures on callbacks.

Mistake: Using decimal format ("10.00") in checksum computation instead of minor units ("1000").

Fix: Always use the smallest currency unit (cents, pence, agorot) as a string:

// WRONG
const checksumString = `${accountId}|10.00|USD|${requestId}`;
 
// CORRECT
const checksumString = `${accountId}|1000|USD|${requestId}`;

See the Checksum Authentication Guide for the full format specification.

#8. Not Handling CUSTOMER_VERIFICATION Status

Symptom: Payment appears stuck, customer never sees 3DS challenge.

Mistake: Only checking for SUCCEED and FAILED — ignoring CUSTOMER_VERIFICATION.

Fix: Handle all three initial response states:

const result = await processPayment(paymentData);
 
switch (result.transactionStatus) {
  case 'SUCCEED':
    showSuccess();
    break;
  case 'FAILED':
    showError(result.declineCode);
    break;
  case 'CUSTOMER_VERIFICATION':
    // Redirect to 3DS challenge page
    window.location.href = result.challengeUrl;
    break;
  default:
    // PENDING — wait for webhook
    showProcessing();
}

#9. Hardcoding Sandbox URLs in Production

Symptom: Payments work in testing but fail in production with connection errors.

Mistake: Forgetting to switch the base URL.

Fix: Use environment variables for the API base URL:

// WRONG
const API_URL = 'https://sandbox.api.exirom.com/api';
 
// CORRECT
const API_URL = process.env.EXIROM_API_URL;
// Set to https://sandbox.api.exirom.com/api in dev
// Set to https://api.exirom.com/api in production

#10. Not Implementing Webhook Deduplication

Symptom: Customer charged once but order fulfilled twice, double inventory deduction.

Mistake: Processing every webhook callback without checking for duplicates.

Fix: Use transactionId + transactionStatus as a deduplication key. See Webhook Best Practices.

#Quick Reference

MistakeImpactPriority
Trust redirect URLsWrong order fulfillmentCritical
No webhook deduplicationDouble fulfillmentCritical
No signature verificationFraud vulnerabilityCritical
Reuse requestIdDuplicate chargesHigh
Missing device data3DS failuresHigh
Auth per requestPerformance, rate limitsMedium
Wrong checksum formatRequest rejectionsMedium
Ignore CUSTOMER_VERIFICATIONStuck paymentsMedium
Hardcoded URLsProduction failuresLow
Slow webhook processingDuplicate webhooksMedium

#See Also

Was this helpful?