Skip to main content
Rebell APIs use a structured error model that clearly separates transport-level errors, business-level outcomes, and payment lifecycle states. You must always interpret both the HTTP status code and the business result object contained in the response body.
A request may return HTTP 200 OK while still representing a business failure. Always check the result object.

Error Layers

Every API response conveys information at three different layers:
LayerDescriptionExample
HTTP Status CodeWhether the request was correctly received and processed at a protocol level200, 400, 500
Business Result Object (result)The outcome of the requested operation at a business levelSUCCESS, PAYMENT_FAIL
Payment Status (paymentStatus / webhook)The final or intermediate state of the payment transactionSUCCESS, FAIL, PROCESSING

Result Object Structure

All Rebell API responses include a result object:
{
  "result": {
    "resultCode": "SUCCESS",
    "resultStatus": "S",
    "resultMessage": "Optional description"
  }
}

Result Fields

resultCode
string
A detailed business outcome code (e.g., SUCCESS, PAYMENT_FAIL, USER_REJECTED)
resultStatus
string
A high-level status flag used to drive merchant logic: S, U, or F
resultMessage
string
An optional, human-readable message intended for debugging or logging (not for end users)

Result Status Values

The resultStatus field provides a fast way to determine how to proceed.
resultStatusMeaningMerchant Action
SSuccessContinue the flow
UProcessing / UnknownWait or use Inquiry API
FFailureAbort payment
Important: Always evaluate resultStatus before inspecting specific result codes. This ensures consistent handling across all error scenarios.

HTTP Status Codes

HTTP status codes indicate whether the API request itself was accepted at the protocol level.
HTTP CodeMeaningShould Retry
200Request processedNo
400Invalid requestNo
401Invalid signature or credentialsNo
403Access deniedNo
429Rate limit exceededYes (with backoff)
500 / 503Temporary system errorYes
Only network-level or 5xx errors should trigger automatic retries. Business failures (HTTP 200 with resultStatus: F) should not be retried.

Common Result Codes

The following result codes are shared across multiple payment flows:
resultCodeDescriptionRecommended Action
SUCCESSOperation completed successfullyContinue
PROCESSINGPayment pendingWait / use Inquiry
PAYMENT_FAILPayment failedDo not retry
USER_REJECTEDUser declined the paymentAllow new attempt
INSUFFICIENT_BALANCEUser has insufficient fundsSuggest top-up
RISK_REJECTRejected by risk or compliance rulesDo not retry
ORDER_STATUS_INVALIDInvalid order stateCreate new order
PAYMENT_NOT_FOUNDPayment does not existVerify IDs / environment
INVALID_CODEInvalid or expired payment codeAsk user to refresh code
QR_EXPIREDQR code has expiredGenerate new QR
PARAM_ILLEGALInvalid request parametersFix request body
ACCESS_DENIEDCredentials don’t match paymentVerify Client-Id
REQUEST_TRAFFIC_EXCEED_LIMITRate limit exceededReduce request frequency

Handling Errors by Payment Flow

Error handling for Retail Pay (merchant scans user QR):
ResultAction
SUCCESSPayment is final, complete sale
PROCESSINGStart Inquiry polling
INVALID_CODEAsk user to refresh payment code in SuperApp
USER_REJECTEDAbort transaction, offer alternative payment
PAYMENT_FAILOffer alternative payment method
INSUFFICIENT_BALANCEInform user, suggest top-up
Retail Pay Error Handling
async function handleRetailPayResponse(response) {
  switch (response.result.resultStatus) {
    case 'S':
      // Payment successful
      return completeTransaction(response.paymentId);

    case 'U':
      // Processing - start polling
      return pollPaymentStatus(response.paymentId);

    case 'F':
      // Failed - handle specific error
      switch (response.result.resultCode) {
        case 'INVALID_CODE':
          return showError('Please refresh your payment code and try again');
        case 'USER_REJECTED':
          return showError('Payment was cancelled');
        case 'INSUFFICIENT_BALANCE':
          return showError('Insufficient balance. Please top up and try again');
        default:
          return showError('Payment failed. Please try another payment method');
      }
  }
}

Retry & Idempotency Rules

Retries must be handled carefully to avoid duplicate payments.
These scenarios are safe to retry:
  • Network timeouts (no response received)
  • HTTP 5xx errors (server-side issues)
  • Same request with the same paymentRequestId
  • Connection failures before response
  • TLS/SSL errors
Safe Retry Logic
async function safeRetry(paymentRequest, maxRetries = 3) {
  for (let attempt = 1; attempt <= maxRetries; attempt++) {
    try {
      const response = await callPaymentAPI(paymentRequest);

      // Got a response - don't retry regardless of business result
      return response;

    } catch (error) {
      if (isNetworkError(error) || isServerError(error)) {
        // Safe to retry with same paymentRequestId
        console.log(`Attempt ${attempt} failed, retrying...`);
        await delay(1000 * attempt); // Exponential backoff
        continue;
      }
      throw error; // Don't retry other errors
    }
  }
  throw new Error('Max retries exceeded');
}
Critical: The paymentRequestId acts as the idempotency key and must be unique per payment attempt. Never generate a new paymentRequestId when retrying the same payment.

Webhook Error Handling

Webhook delivery is at-least-once. Your webhook handlers must be idempotent.
ScenarioAction
Duplicate eventsIgnore and return HTTP 200
Invalid signaturesReject and log (return 4xx)
Temporary internal errorsReturn non-2xx to trigger retry
Unknown event typesLog and return HTTP 200
Webhook Error Handling
app.post('/webhooks/rebell', async (req, res) => {
  // Verify signature first
  if (!verifySignature(req)) {
    console.error('Invalid webhook signature');
    return res.status(401).send('Invalid signature');
  }

  try {
    const { paymentId, paymentStatus } = req.body;

    // Check for duplicate (idempotency)
    const existing = await db.payments.findOne({ paymentId });
    if (existing?.status === 'paid' || existing?.status === 'failed') {
      console.log('Duplicate webhook, ignoring');
      return res.status(200).send('OK');
    }

    // Process the event
    await processPaymentResult(req.body);
    return res.status(200).send('OK');

  } catch (error) {
    console.error('Webhook processing error:', error);
    // Return 5xx to trigger retry
    return res.status(500).send('Internal error');
  }
});
Webhooks always represent the final payment outcome. See Webhooks for complete implementation details.

User-Facing Error Messages

Technical error codes should never be exposed directly to end users. Map them to friendly messages:
resultCodeUser Message
USER_REJECTED”Payment cancelled”
INSUFFICIENT_BALANCE”Insufficient balance. Please top up your account.”
PROCESSING”Waiting for confirmation…”
PAYMENT_FAIL”Payment failed. Please try again.”
INVALID_CODE”Payment code expired. Please refresh and try again.”
QR_EXPIRED”QR code expired. Please scan the new code.”
RISK_REJECT”Payment could not be completed. Please try another method.”
Network errors”Connection error. Please check your internet and try again.”
User Message Mapping
const userMessages = {
  USER_REJECTED: 'Payment cancelled',
  INSUFFICIENT_BALANCE: 'Insufficient balance. Please top up your account.',
  PROCESSING: 'Waiting for confirmation...',
  PAYMENT_FAIL: 'Payment failed. Please try again.',
  INVALID_CODE: 'Payment code expired. Please refresh and try again.',
  QR_EXPIRED: 'QR code expired. Please scan the new code.',
  RISK_REJECT: 'Payment could not be completed. Please try another method.',
  ORDER_STATUS_INVALID: 'This order is no longer valid. Please start again.',
  PAYMENT_NOT_FOUND: 'Payment not found. Please contact support.',
  default: 'Something went wrong. Please try again.'
};

function getUserMessage(resultCode) {
  return userMessages[resultCode] || userMessages.default;
}

Complete Error Handling Example

Here’s a comprehensive error handling implementation:
Complete Error Handler
class PaymentErrorHandler {
  constructor() {
    this.maxRetries = 3;
    this.retryDelay = 1000;
  }

  async handlePaymentResponse(response, context) {
    const { resultStatus, resultCode, resultMessage } = response.result;

    // Log for debugging
    console.log(`Payment response: ${resultCode} (${resultStatus})`, {
      paymentId: response.paymentId,
      context,
      message: resultMessage
    });

    switch (resultStatus) {
      case 'S':
        return this.handleSuccess(response, context);
      case 'U':
        return this.handleProcessing(response, context);
      case 'F':
        return this.handleFailure(response, context);
      default:
        throw new Error(`Unknown resultStatus: ${resultStatus}`);
    }
  }

  handleSuccess(response, context) {
    return {
      success: true,
      paymentId: response.paymentId,
      action: 'complete',
      userMessage: 'Payment successful!'
    };
  }

  handleProcessing(response, context) {
    return {
      success: false,
      pending: true,
      paymentId: response.paymentId,
      action: 'poll',
      userMessage: 'Waiting for confirmation...'
    };
  }

  handleFailure(response, context) {
    const { resultCode } = response.result;

    // Determine if retry is allowed
    const canRetry = !['USER_REJECTED', 'RISK_REJECT', 'INSUFFICIENT_BALANCE']
      .includes(resultCode);

    return {
      success: false,
      pending: false,
      error: resultCode,
      canRetry,
      action: canRetry ? 'retry' : 'abort',
      userMessage: this.getUserMessage(resultCode)
    };
  }

  getUserMessage(resultCode) {
    const messages = {
      USER_REJECTED: 'Payment cancelled',
      INSUFFICIENT_BALANCE: 'Insufficient balance',
      RISK_REJECT: 'Payment could not be completed',
      INVALID_CODE: 'Please refresh your payment code',
      QR_EXPIRED: 'QR code expired',
      PAYMENT_FAIL: 'Payment failed. Please try again.'
    };
    return messages[resultCode] || 'Something went wrong. Please try again.';
  }

  shouldRetryRequest(error) {
    // Network errors - safe to retry
    if (error.code === 'ECONNRESET' || error.code === 'ETIMEDOUT') {
      return true;
    }
    // 5xx server errors - safe to retry
    if (error.status >= 500) {
      return true;
    }
    // 429 rate limit - retry with backoff
    if (error.status === 429) {
      return true;
    }
    return false;
  }
}

Testing Checklist

Test these error scenarios in sandbox before going live:

Next Steps