PlexySDK DOCS

Result Codes

Understanding payment result codes and how to handle each outcome

Result Codes

When you make a payment request, Plexy returns a resultCode that indicates the outcome. Understanding these codes is essential for providing a good customer experience and correctly handling payment states.

Quick Reference

Result CodeMeaningAction Required
AuthorisedPayment successfulFulfill the order
RefusedPayment declinedShow decline message
PendingAwaiting final resultWait for webhook
ReceivedPayment initiatedWait for webhook
ErrorTechnical errorRetry or investigate
CancelledCustomer cancelledReturn to checkout
RedirectShopperRedirect requiredRedirect the customer
IdentifyShopper3DS device fingerprintPerform 3DS fingerprint
ChallengeShopper3DS challenge requiredShow 3DS challenge
PresentToShopperShow voucher/QRDisplay payment info

Successful Payment

Authorised

The payment was successfully authorized. The funds have been reserved on the customer's account (or captured, depending on your configuration).

What to do:

  • Mark the order as paid
  • Trigger order fulfillment (shipping, access, etc.)
  • Send confirmation email to customer
if (payment.resultCode === 'Authorised') {
  await db.orders.update({
    where: { id: orderId },
    data: {
      status: 'paid',
      paymentId: payment.pspReference
    }
  });

  await fulfillOrder(orderId);
  await sendConfirmationEmail(orderId);
}

If you have separate authorization and capture, the payment is only reserved. You'll need to capture it later to receive the funds.

Declined Payments

Refused

The payment was declined by the card issuer, payment provider, or Plexy's risk system.

What to do:

  • Display a customer-friendly message
  • Suggest trying a different payment method
  • Do not store the card details for retry
if (payment.resultCode === 'Refused') {
  const customerMessage = getRefusalMessage(payment.refusalReasonCode);

  return {
    status: 'refused',
    message: customerMessage,
    // Log the full reason for debugging
    internalReason: payment.refusalReason
  };
}

function getRefusalMessage(refusalCode) {
  const messages = {
    '2': 'The transaction was declined. Please try a different payment method.',
    '3': 'Invalid card details. Please check and try again.',
    '4': 'Invalid PIN. Please try again.',
    '5': 'Card expired. Please use a different card.',
    '6': 'Invalid CVV. Please check and try again.',
    '7': 'Insufficient funds. Please try a different card.',
    '8': 'Card blocked. Please contact your bank.',
    '20': 'Fraud detected. Transaction declined.',
  };

  return messages[refusalCode] || 'Payment declined. Please try a different payment method.';
}

Common Refusal Reason Codes

CodeReasonCustomer Message
2RefusedTry a different payment method
3ReferralContact bank for approval
4Acquirer ErrorTechnical issue, retry later
5Blocked CardCard is blocked by issuer
6Expired CardCard has expired
7Invalid AmountAmount issue, contact support
8Invalid Card NumberCheck card number
9Issuer UnavailableBank unavailable, retry later
10Not SupportedCard type not accepted
113D Secure FailedAuthentication failed
12Not Enough BalanceInsufficient funds
14FraudPayment declined
15CancelledCustomer cancelled
16Shopper CancelledCustomer cancelled
17Invalid PinIncorrect PIN
20Fraud - RiskDeclined by risk system
21Not SubmittedProcessing error
22Fraud - CVVCVV check failed
23Transaction Not PermittedNot allowed for this card
24CVC DeclinedCVV check failed
25Restricted CardCard restricted
26RevocationAuthorization revoked
27Declined Non-GenericContact bank
28Withdrawal Amount ExceededOver withdrawal limit
29Withdrawal Count ExceededToo many withdrawals
31Fraud - RawFraud detected
32AVS DeclinedAddress verification failed
33Card requires online PINOnline PIN required
34No checking accountAccount not found
35No savings accountAccount not found
36Mobile PIN requiredPIN required
37Contactless fallbackUse chip instead
38Authentication requiredAdditional auth needed

Cancelled

The customer cancelled the payment before completion. This typically happens when:

  • Customer clicks "back" or closes the payment page
  • Customer cancels on a redirect page (e.g., bank authentication)
  • Session times out

What to do:

  • Return customer to the cart/checkout page
  • Keep the order in "pending" state
  • Allow retry with same or different payment method
if (payment.resultCode === 'Cancelled') {
  return {
    status: 'cancelled',
    message: 'Payment was cancelled. Would you like to try again?',
    redirectTo: '/checkout'
  };
}

Error

A technical error occurred during payment processing. This could be due to:

  • Network issues
  • Service unavailability
  • Invalid request data
  • Configuration issues

What to do:

  • Log the error for investigation
  • Display a generic error message
  • Offer retry option
  • Alert your team if errors persist
if (payment.resultCode === 'Error') {
  console.error('Payment error:', {
    pspReference: payment.pspReference,
    errorCode: payment.errorCode,
    message: payment.errorMessage
  });

  // Alert if this is a recurring issue
  if (await isRecurringError(payment.errorCode)) {
    await alertTeam('Recurring payment error', payment);
  }

  return {
    status: 'error',
    message: 'Something went wrong. Please try again or use a different payment method.',
    canRetry: true
  };
}

Pending States

Pending

The payment result is not yet known. This is common for:

  • Bank transfers
  • Asynchronous payment methods
  • Manual review

What to do:

  • Mark the order as "awaiting payment"
  • Wait for webhook notification with final result
  • Show appropriate status to customer
if (payment.resultCode === 'Pending') {
  await db.orders.update({
    where: { id: orderId },
    data: {
      status: 'awaiting_payment',
      paymentId: payment.pspReference
    }
  });

  return {
    status: 'pending',
    message: 'Your payment is being processed. We\'ll notify you once it\'s confirmed.'
  };
}

Never fulfill orders with Pending status until you receive a webhook confirming the payment was successful.

Received

Similar to Pending, but specifically indicates that Plexy has received the payment request. The final result will come via webhook.

What to do:

  • Same as Pending
  • Common for payment methods like Boleto, iDEAL, or bank transfers
if (payment.resultCode === 'Received') {
  await db.orders.update({
    where: { id: orderId },
    data: { status: 'payment_initiated' }
  });

  return {
    status: 'processing',
    message: 'Payment initiated. You\'ll receive confirmation shortly.'
  };
}

Action Required

These result codes indicate that additional steps are needed to complete the payment.

RedirectShopper

The customer must be redirected to an external page to complete authentication or authorization (e.g., bank login, payment app).

What to do:

  • Store relevant state (order ID) for when customer returns
  • Redirect to the URL in the action object
  • Handle the return URL callback
if (payment.resultCode === 'RedirectShopper') {
  const { action } = payment;

  // Store session data
  await db.paymentSessions.create({
    data: {
      orderId: orderId,
      paymentData: action.paymentData,
      createdAt: new Date()
    }
  });

  return {
    status: 'redirect',
    url: action.url,
    method: action.method,
    data: action.data // For POST redirects
  };
}

IdentifyShopper

3D Secure 2 device fingerprinting is required. This is the first step of 3DS2 authentication.

What to do:

  • Use the Plexy 3DS SDK to perform fingerprinting
  • Submit the result to /payments/details
  • Handle the next result code
if (payment.resultCode === 'IdentifyShopper') {
  return {
    status: '3ds_fingerprint',
    action: payment.action
  };
}

// Client-side handling
const threeDS = new Plexy3DS({ clientKey: 'YOUR_CLIENT_KEY' });
threeDS.handleAction(action);

ChallengeShopper

3D Secure 2 challenge is required. The customer needs to authenticate with their bank (e.g., enter OTP, approve in banking app).

What to do:

  • Display the 3DS challenge UI
  • Submit the challenge result to /payments/details
if (payment.resultCode === 'ChallengeShopper') {
  return {
    status: '3ds_challenge',
    action: payment.action
  };
}

// Client-side: Show the challenge
const threeDS = new Plexy3DS({ clientKey: 'YOUR_CLIENT_KEY' });
const challengeComponent = threeDS.create('threeDS2Challenge', {
  challengeWindowSize: '05',
  onComplete: (result) => submitDetails(result.data),
  onError: (error) => handleError(error)
});
challengeComponent.handleAction(action);

PresentToShopper

Payment information needs to be shown to the customer (e.g., voucher, QR code, bank transfer details).

What to do:

  • Display the provided payment information
  • Wait for webhook notification of payment completion
  • Some methods have expiration times
if (payment.resultCode === 'PresentToShopper') {
  const { action } = payment;

  return {
    status: 'show_voucher',
    data: {
      type: action.paymentMethodType,
      reference: action.reference,
      expiresAt: action.expiresAt,
      downloadUrl: action.downloadUrl,
      // For QR codes
      qrCodeData: action.qrCodeData,
      // For bank transfers
      bankDetails: {
        iban: action.iban,
        bic: action.bic,
        accountHolder: action.accountHolder
      }
    }
  };
}

Comprehensive Result Handler

Here's a complete example handling all result codes:

async function handlePaymentResult(payment, orderId) {
  const { resultCode } = payment;

  switch (resultCode) {
    // Success
    case 'Authorised':
      await updateOrderStatus(orderId, 'paid');
      await fulfillOrder(orderId);
      return { status: 'success', redirect: '/order/confirmed' };

    // Declined
    case 'Refused':
      await updateOrderStatus(orderId, 'payment_failed');
      return {
        status: 'refused',
        message: getRefusalMessage(payment.refusalReasonCode),
        redirect: '/checkout?error=refused'
      };

    case 'Cancelled':
      return { status: 'cancelled', redirect: '/checkout' };

    case 'Error':
      logError(payment);
      return {
        status: 'error',
        message: 'Technical error. Please try again.',
        redirect: '/checkout?error=technical'
      };

    // Pending
    case 'Pending':
    case 'Received':
      await updateOrderStatus(orderId, 'awaiting_payment');
      return {
        status: 'pending',
        message: 'Payment is being processed...',
        redirect: '/order/pending'
      };

    // Action required
    case 'RedirectShopper':
      await savePaymentData(orderId, payment.action.paymentData);
      return { status: 'redirect', action: payment.action };

    case 'IdentifyShopper':
      return { status: '3ds_fingerprint', action: payment.action };

    case 'ChallengeShopper':
      return { status: '3ds_challenge', action: payment.action };

    case 'PresentToShopper':
      await updateOrderStatus(orderId, 'awaiting_payment');
      return { status: 'voucher', action: payment.action };

    default:
      console.warn('Unknown result code:', resultCode);
      return { status: 'unknown', resultCode };
  }
}
async def handle_payment_result(payment, order_id: str) -> dict:
    result_code = payment.result_code

    # Success
    if result_code == 'Authorised':
        await update_order_status(order_id, 'paid')
        await fulfill_order(order_id)
        return {'status': 'success', 'redirect': '/order/confirmed'}

    # Declined
    if result_code == 'Refused':
        await update_order_status(order_id, 'payment_failed')
        return {
            'status': 'refused',
            'message': get_refusal_message(payment.refusal_reason_code),
            'redirect': '/checkout?error=refused'
        }

    if result_code == 'Cancelled':
        return {'status': 'cancelled', 'redirect': '/checkout'}

    if result_code == 'Error':
        log_error(payment)
        return {
            'status': 'error',
            'message': 'Technical error. Please try again.',
            'redirect': '/checkout?error=technical'
        }

    # Pending
    if result_code in ['Pending', 'Received']:
        await update_order_status(order_id, 'awaiting_payment')
        return {
            'status': 'pending',
            'message': 'Payment is being processed...',
            'redirect': '/order/pending'
        }

    # Action required
    if result_code == 'RedirectShopper':
        await save_payment_data(order_id, payment.action['paymentData'])
        return {'status': 'redirect', 'action': payment.action}

    if result_code == 'IdentifyShopper':
        return {'status': '3ds_fingerprint', 'action': payment.action}

    if result_code == 'ChallengeShopper':
        return {'status': '3ds_challenge', 'action': payment.action}

    if result_code == 'PresentToShopper':
        await update_order_status(order_id, 'awaiting_payment')
        return {'status': 'voucher', 'action': payment.action}

    # Unknown
    print(f'Unknown result code: {result_code}')
    return {'status': 'unknown', 'resultCode': result_code}

Webhook Event Types

Each result code maps to webhook events you should handle:

Result CodeWebhook Event
Authorisedpayment.authorised
Refusedpayment.refused
Cancelledpayment.cancelled
Errorpayment.error
Pending → Successpayment.authorised
Pending → Failurepayment.refused or payment.expired
Received → Successpayment.authorised
Received → Failurepayment.refused or payment.expired

Best Practices

  1. Always verify via webhooks - Don't rely solely on the synchronous response. Webhooks provide the authoritative payment status.

  2. Map to your order states - Create a clear mapping between Plexy result codes and your internal order states.

  3. Log all results - Keep detailed logs of payment results for debugging and support.

  4. Show appropriate messages - Use customer-friendly messages for declines. Avoid exposing raw error codes.

  5. Handle edge cases - Plan for network failures, timeouts, and unexpected result codes.

  6. Test all scenarios - Use test cards to simulate different result codes in test mode.

Next Steps

On this page