Error Response Structure
All errors follow a consistent format:
{
"success" : false ,
"error" : {
"code" : "ERROR_CODE" ,
"message" : "Human-readable error message" ,
"details" : {
// additional context
}
},
"requestId" : "req_abc123"
}
HTTP Status Codes
Status Category Description
400Client Error Invalid request parameters or validation error 401Authentication Invalid or missing API key 403Authorization Insufficient permissions or balance 404Not Found Resource doesn’t exist 429Rate Limit Too many requests 500Server Error Internal server error 503Unavailable Service temporarily unavailable
Common Error Codes
Authentication Errors (401)
MISSING_ACCESS_TOKEN
Message : Access token is required
Cause : The X-Access-Token header is missing
Action : Include your API key in the header
// ✅ Correct
fetch ( url , {
headers: { 'X-Access-Token' : 'YOUR_API_KEY' }
})
INVALID_ACCESS_TOKEN / ERR_AUTH
Message : The provided access token is invalid
Cause : API key is incorrect, expired, or revoked
Action :
Verify API key is correct
Generate a new key if needed
Don’t log sensitive key values
Contact support if issue persists
Authorization Errors (403)
NO_BALANCE / INSUFFICIENT_BALANCE
Message : Insufficient balance
Cause : Account balance is too low
Action :
Check balance using /v3/account/balance before operations
Display current balance to user
Offer top-up option
Don’t retry without adding funds
const balanceRes = await fetch ( 'https://api.oneclickdz.com/v3/account/balance' , {
headers: { 'X-Access-Token' : API_KEY }
});
const { data } = await balanceRes . json ();
if ( data . balance < requiredAmount ) {
throw new Error ( 'Insufficient balance' );
}
DUPLICATED_REF
Message : This reference ID is already in use
Cause : Reference already used in previous request
Action :
Check status of existing order
Generate new unique reference
Don’t create duplicate orders
const ref = `order- ${ Date . now () } - ${ Math . random (). toString ( 36 ). substr ( 2 , 9 ) } ` ;
IP_BLOCKED
Message : Your IP has been temporarily blocked
Cause : Too many failed authentication attempts
Action :
Wait 15 minutes for automatic unblock
Verify correct API key
Contact support if persists
IP_NOT_ALLOWED
Message : Your IP address is not whitelisted
Cause : IP whitelisting is enabled
Action : Add your IP to whitelist in dashboard
ERR_VALIDATION
Message : Validation error
Cause : Request parameters don’t meet requirements
Common Issues : Missing fields, invalid data types, pattern mismatch
Action :
Validate input client-side first
Check error.details for specific field issues
Don’t retry without fixing the issue
// Validate before sending
function validateTopupRequest ( data ) {
const errors = [];
if ( ! data . plan_code ) {
errors . push ({ field: 'plan_code' , message: 'Plan code is required' });
}
if ( ! data . MSSIDN || ! / ^ 0 [ 567 ][ 0-9 ] {8} $ / . test ( data . MSSIDN )) {
errors . push ({ field: 'MSSIDN' , message: 'Invalid phone number format' });
}
if ( isDynamicPlan ( data . plan_code )) {
if ( ! data . amount || data . amount < 50 || data . amount > 5000 ) {
errors . push ({ field: 'amount' , message: 'Amount must be between 50 and 5000' });
}
}
return errors . length > 0 ? { valid: false , errors } : { valid: true };
}
ERR_PHONE
Message : Invalid phone number
Cause : Phone number is incorrect or doesn’t exist
Action : Use /v3/internet/check-number to validate first
const checkRes = await fetch (
`https://api.oneclickdz.com/v3/internet/check-number?type=ADSL&number= ${ number } ` ,
{ headers: { 'X-Access-Token' : API_KEY } }
);
if ( ! checkRes . ok ) {
throw new Error ( 'Invalid phone number' );
}
ERR_STOCK
Message : Product out of stock
Cause : Requested product/card value is not available
Action :
Check stock before ordering
Offer alternative denominations
Retry later
NOT_FOUND - Message : Resource not found - Cause : The requested resource doesn’t exist - Common Cases : Invalid order ID, invalid
reference, deleted resource - Action : - Verify the ID/reference is correct
Check for typos - Handle gracefully in UI
RATE_LIMIT_EXCEEDED
Message : Too many requests
Cause : Exceeded the rate limit
Limits : Sandbox: 60 req/min, Production: 120 req/min
Action : Implement exponential backoff with retry logic
async function fetchWithRetry ( url , options , maxRetries = 3 ) {
for ( let i = 0 ; i < maxRetries ; i ++ ) {
const response = await fetch ( url , options );
if ( response . status === 429 ) {
const retryAfter = response . headers . get ( 'Retry-After' ) || Math . pow ( 2 , i );
await new Promise ( resolve => setTimeout ( resolve , retryAfter * 1000 ));
continue ;
}
return response ;
}
throw new Error ( 'Max retries exceeded' );
}
INTERNAL_SERVER_ERROR / INTERNAL_ERROR
Message : Developer was notified and will check shortly
Cause : Unexpected error on our servers
Action :
Don’t refund immediately - wait 24 hours
Save requestId for support
Implement retry logic with backoff
Contact support with details
We’re automatically notified
if ( response . status === 500 ) {
console . error ( 'Server error:' , data . requestId );
// Mark for review - don't refund yet
await scheduleStatusCheck ( orderId , 24 * 60 * 60 * 1000 );
}
ERR_SERVICE
Message : Service temporarily unavailable
Cause : Service maintenance or temporary issue
Action :
Show maintenance message
Retry after delay
Monitor for resolution
Implementation Best Practices
1. Always Check Success Field
const response = await fetch ( url , options );
const data = await response . json ();
if ( data . success ) {
return data . data ;
} else {
throw new Error ( ` ${ data . error . code } : ${ data . error . message } ` );
}
2. Handle Specific Error Codes
async function sendTopUp ( params ) {
const response = await fetch ( url , {
method: "POST" ,
headers: {
"Content-Type" : "application/json" ,
"X-Access-Token" : API_KEY ,
},
body: JSON . stringify ( params ),
});
const data = await response . json ();
if ( ! data . success ) {
switch ( data . error . code ) {
case "NO_BALANCE" :
case "INSUFFICIENT_BALANCE" :
throw new Error ( "Insufficient balance. Please add funds." );
case "DUPLICATED_REF" :
return await checkOrderStatus ( params . ref );
case "ERR_VALIDATION" :
throw new Error ( `Invalid input: ${ data . error . message } ` );
case "INTERNAL_SERVER_ERROR" :
case "INTERNAL_ERROR" :
await scheduleStatusCheck ( params . ref );
throw new Error ( "Temporary error, checking status later" );
default :
throw new Error ( ` ${ data . error . code } : ${ data . error . message } ` );
}
}
return data . data ;
}
3. Smart Retry Logic with Exponential Backoff
async function safeApiCall ( apiFunction , options = {}) {
const { maxRetries = 3 , retryDelay = 1000 , retryOn = [ 500 , 503 ] } = options ;
let lastError ;
for ( let attempt = 1 ; attempt <= maxRetries ; attempt ++ ) {
try {
return await apiFunction ();
} catch ( error ) {
lastError = error ;
// Don't retry on client errors (4xx)
if ( error . status >= 400 && error . status < 500 ) {
throw error ;
}
// Retry on specific status codes
if ( retryOn . includes ( error . status ) && attempt < maxRetries ) {
const delay = retryDelay * Math . pow ( 2 , attempt - 1 );
console . log ( `Retry ${ attempt } / ${ maxRetries } after ${ delay } ms` );
await new Promise (( resolve ) => setTimeout ( resolve , delay ));
continue ;
}
throw error ;
}
}
throw lastError ;
}
// Usage
try {
const result = await safeApiCall (() => sendMobileTopup ( data ), {
maxRetries: 3 ,
retryDelay: 2000 ,
});
} catch ( error ) {
handleError ( error );
}
4. User-Friendly Error Messages
const errorMessages = {
MISSING_ACCESS_TOKEN: "Authentication required. Please check your API key." ,
INVALID_ACCESS_TOKEN: "Invalid API key. Please verify your credentials." ,
ERR_AUTH: "Authentication failed. Please check your API key." ,
NO_BALANCE: "Insufficient balance. Please top up your account." ,
INSUFFICIENT_BALANCE: "Insufficient balance. Please top up your account." ,
DUPLICATED_REF: "This order was already processed." ,
ERR_VALIDATION: "Please check your input and try again." ,
ERR_PHONE: "Invalid phone number. Please verify and try again." ,
ERR_STOCK: "This product is currently out of stock." ,
NOT_FOUND: "The requested resource was not found." ,
RATE_LIMIT_EXCEEDED: "Too many requests. Please wait a moment and try again." ,
ERR_SERVICE: "Service temporarily unavailable. Please try again." ,
INTERNAL_SERVER_ERROR: "An error occurred. Our team has been notified." ,
INTERNAL_ERROR: "An error occurred. Our team has been notified." ,
};
function getUserFriendlyMessage ( errorCode , defaultMessage ) {
return (
errorMessages [ errorCode ] ||
defaultMessage ||
"An unexpected error occurred."
);
}
// Usage
if ( ! data . success ) {
const userMessage = getUserFriendlyMessage (
data . error . code ,
data . error . message
);
showErrorToUser ( userMessage );
}
5. Log Request IDs
function logError ( error , context ) {
const errorLog = {
timestamp: new Date (). toISOString (),
requestId: error . requestId ,
code: error . code ,
message: error . message ,
context: {
userId: context . userId ,
operation: context . operation ,
// Don't log sensitive data
},
};
// Log to your logging service
logger . error ( "API Error" , errorLog );
// Alert on critical errors
if ( error . status >= 500 ) {
alertOps ( "API Error" , errorLog );
}
}
Advanced Patterns
Circuit Breaker Pattern
Prevent cascading failures by stopping requests when error rate is high:
class CircuitBreaker {
constructor ( threshold = 5 , timeout = 60000 ) {
this . failureCount = 0 ;
this . threshold = threshold ;
this . timeout = timeout ;
this . state = "CLOSED" ; // CLOSED, OPEN, HALF_OPEN
this . nextAttempt = Date . now ();
}
async execute ( fn ) {
if ( this . state === "OPEN" ) {
if ( Date . now () < this . nextAttempt ) {
throw new Error ( "Circuit breaker is OPEN" );
}
this . state = "HALF_OPEN" ;
}
try {
const result = await fn ();
this . onSuccess ();
return result ;
} catch ( error ) {
this . onFailure ();
throw error ;
}
}
onSuccess () {
this . failureCount = 0 ;
this . state = "CLOSED" ;
}
onFailure () {
this . failureCount ++ ;
if ( this . failureCount >= this . threshold ) {
this . state = "OPEN" ;
this . nextAttempt = Date . now () + this . timeout ;
}
}
}
// Usage
const breaker = new CircuitBreaker ( 5 , 60000 );
try {
const result = await breaker . execute (() => sendTopup ( data ));
} catch ( error ) {
if ( error . message === "Circuit breaker is OPEN" ) {
showMaintenanceMessage ();
}
}
Error Monitoring
Track error patterns to identify issues early:
class ApiClient {
constructor ( apiKey ) {
this . apiKey = apiKey ;
this . errorStats = new Map ();
}
trackError ( errorCode ) {
const count = this . errorStats . get ( errorCode ) || 0 ;
this . errorStats . set ( errorCode , count + 1 );
}
async request ( url , options ) {
try {
const response = await fetch ( url , {
... options ,
headers: {
... options . headers ,
"X-Access-Token" : this . apiKey ,
},
});
const data = await response . json ();
if ( ! data . success ) {
this . trackError ( data . error . code );
// Alert if too many errors
if ( this . errorStats . get ( data . error . code ) > 10 ) {
this . sendAlert ( `High error rate: ${ data . error . code } ` );
}
}
return data ;
} catch ( error ) {
this . trackError ( "NETWORK_ERROR" );
throw error ;
}
}
sendAlert ( message ) {
console . error ( "ALERT:" , message );
// Send to your monitoring service
}
}
Handling UNKNOWN_ERROR
Critical: Never refund immediately on UNKNOWN_ERROR. Always wait 24 hours
for resolution.
async function handleUnknownError ( orderId , topupId ) {
// Mark for review
await db . orders . update ({
where: { id: orderId },
data: {
status: "REVIEW_NEEDED" ,
reviewReason: "UNKNOWN_ERROR from API" ,
reviewScheduled: new Date ( Date . now () + 24 * 60 * 60 * 1000 ),
},
});
// Show message to user
await notifyUser ( orderId , {
title: "Order Under Review" ,
message:
"Your order is being verified. Status will update within 24 hours." ,
});
// Schedule automatic recheck
await scheduleJob ( "recheck-unknown-error" , {
orderId ,
topupId ,
runAt: new Date ( Date . now () + 24 * 60 * 60 * 1000 ),
});
}
Testing Error Scenarios
Use sandbox mode to test error handling:
// Test insufficient balance
// Set your sandbox balance very low
// Test validation errors
await sendTopUp ({
plan_code: 'INVALID_PLAN' ,
MSSIDN: '123456789' , // Invalid format
amount: - 100 // Negative amount
});
// Test duplicate reference
const ref = 'test-duplicate-123' ;
await sendTopUp ({ ref , ... }); // First request
await sendTopUp ({ ref , ... }); // Should fail with DUPLICATED_REF
Quick Reference
Validate Early Validate input client-side before API calls to catch errors early
Retry Smart Use exponential backoff for 5xx errors, never retry 4xx errors
Log Context Always include requestId and context in logs for debugging
User Feedback Show clear, actionable error messages to users
Next Steps