Common Integration Mistakes
Avoid these common pitfalls when integrating with the Exirom API
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
| Mistake | Impact | Priority |
|---|---|---|
| Trust redirect URLs | Wrong order fulfillment | Critical |
| No webhook deduplication | Double fulfillment | Critical |
| No signature verification | Fraud vulnerability | Critical |
| Reuse requestId | Duplicate charges | High |
| Missing device data | 3DS failures | High |
| Auth per request | Performance, rate limits | Medium |
| Wrong checksum format | Request rejections | Medium |
| Ignore CUSTOMER_VERIFICATION | Stuck payments | Medium |
| Hardcoded URLs | Production failures | Low |
| Slow webhook processing | Duplicate webhooks | Medium |
#See Also
- Error Recovery Patterns -- handling failed, stuck, and ambiguous transactions
- Troubleshooting -- solutions for common integration issues