# Result Codes and Errors

## 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 Code | Meaning | Action Required |
|-------------|---------|-----------------|
| `Authorised` | Payment successful | Fulfill the order |
| `Refused` | Payment declined | Show decline message |
| `Pending` | Awaiting final result | Wait for webhook |
| `Received` | Payment initiated | Wait for webhook |
| `Error` | Technical error | Retry or investigate |
| `Cancelled` | Customer cancelled | Return to checkout |
| `RedirectShopper` | Redirect required | Redirect the customer |
| `IdentifyShopper` | 3DS device fingerprint | Perform 3DS fingerprint |
| `ChallengeShopper` | 3DS challenge required | Show 3DS challenge |
| `PresentToShopper` | Show voucher/QR | Display 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

```javascript
if (payment.resultCode === 'Authorised') {
  await db.orders.update({
    where: { id: orderId },
    data: {
      status: 'paid',
      paymentId: payment.pspReference
    }
  });

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

:::info[Info]
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

```javascript
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

| Code | Reason | Customer Message |
|------|--------|------------------|
| `2` | Refused | Try a different payment method |
| `3` | Referral | Contact bank for approval |
| `4` | Acquirer Error | Technical issue, retry later |
| `5` | Blocked Card | Card is blocked by issuer |
| `6` | Expired Card | Card has expired |
| `7` | Invalid Amount | Amount issue, contact support |
| `8` | Invalid Card Number | Check card number |
| `9` | Issuer Unavailable | Bank unavailable, retry later |
| `10` | Not Supported | Card type not accepted |
| `11` | 3D Secure Failed | Authentication failed |
| `12` | Not Enough Balance | Insufficient funds |
| `14` | Fraud | Payment declined |
| `15` | Cancelled | Customer cancelled |
| `16` | Shopper Cancelled | Customer cancelled |
| `17` | Invalid Pin | Incorrect PIN |
| `20` | Fraud - Risk | Declined by risk system |
| `21` | Not Submitted | Processing error |
| `22` | Fraud - CVV | CVV check failed |
| `23` | Transaction Not Permitted | Not allowed for this card |
| `24` | CVC Declined | CVV check failed |
| `25` | Restricted Card | Card restricted |
| `26` | Revocation | Authorization revoked |
| `27` | Declined Non-Generic | Contact bank |
| `28` | Withdrawal Amount Exceeded | Over withdrawal limit |
| `29` | Withdrawal Count Exceeded | Too many withdrawals |
| `31` | Fraud - Raw | Fraud detected |
| `32` | AVS Declined | Address verification failed |
| `33` | Card requires online PIN | Online PIN required |
| `34` | No checking account | Account not found |
| `35` | No savings account | Account not found |
| `36` | Mobile PIN required | PIN required |
| `37` | Contactless fallback | Use chip instead |
| `38` | Authentication required | Additional 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

```javascript
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

```javascript
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

```javascript
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.'
  };
}
```

:::warning[Warning]
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

```javascript
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

```javascript
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 `/v2/payments/details`
- Handle the next result code

```javascript
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 `/v2/payments/details`

```javascript
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

```javascript
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:

<Tabs>
  <Tab title="JavaScript">
    ```javascript
    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 };
      }
    }
    ```
  </Tab>
  <Tab title="Python">
    ```python
    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}
    ```
  </Tab>
</Tabs>

### Webhook Event Types

Each result code maps to webhook events you should handle:

| Result Code | Webhook Event |
|-------------|---------------|
| `Authorised` | `payment.authorised` |
| `Refused` | `payment.refused` |
| `Cancelled` | `payment.cancelled` |
| `Error` | `payment.error` |
| `Pending` → Success | `payment.authorised` |
| `Pending` → Failure | `payment.refused` or `payment.expired` |
| `Received` → Success | `payment.authorised` |
| `Received` → Failure | `payment.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. **Verify all scenarios** - Simulate different result codes before going live.

### Next Steps

- Handle 3D Secure authentication flows
- Set up webhooks for reliable notifications
- Verify the full payment lifecycle before enabling customer traffic

---

## HTTP Status Codes

The Plexy API uses standard HTTP status codes to indicate request outcomes.

### Success codes

| Code | Description |
|------|-------------|
| `200 OK` | Request succeeded |
| `201 Created` | Resource created successfully |
| `204 No Content` | Request succeeded, no response body |

### Client error codes

| Code | Description | Action |
|------|-------------|--------|
| `400 Bad Request` | Invalid request parameters | Check request body |
| `401 Unauthorized` | Invalid or missing API key | Verify API credentials |
| `403 Forbidden` | Insufficient permissions | Check API key permissions |
| `404 Not Found` | Resource doesn't exist | Verify resource ID |
| `405 Method Not Allowed` | Invalid HTTP method | Check endpoint documentation |
| `409 Conflict` | Idempotency conflict | Request already processed |
| `422 Unprocessable Entity` | Valid request but cannot process | Check business logic |

### Server error codes

| Code | Description | Action |
|------|-------------|--------|
| `500 Internal Server Error` | Unexpected server error | Retry with backoff |
| `502 Bad Gateway` | Upstream service error | Retry with backoff |
| `503 Service Unavailable` | Service temporarily down | Retry with backoff |
| `504 Gateway Timeout` | Request timed out | Retry with backoff |

### Handling responses

Check the HTTP status code of every response. Successful operations return `2xx`; errors return `4xx` or `5xx` with an `error` object in the body:

```bash
HTTP/2 400
Content-Type: application/json

{
  "error": {
    "type": "invalid_request_error",
    "code": "parameter_missing",
    "message": "Missing required parameter: amount"
  }
}
```

| Status range | Meaning | Action |
|-------------|---------|--------|
| `2xx` | Success | Process the response |
| `400` | Bad request | Fix request parameters |
| `401` | Unauthorized | Verify API key |
| `5xx` | Server error | Retry with exponential backoff |

### Retry strategy

For `500`, `502`, `503`, and `504` responses, retry the request with exponential backoff and the **same** `Idempotency-Key`:

```bash
# Attempt 1 — wait 1 s on failure, then attempt 2 — wait 2 s, then attempt 3
curl -X POST https://api.plexypay.com/v2/payments \
  -H "x-api-key: YOUR_API_KEY" \
  -H "Idempotency-Key: order_12345_payment" \
  -H "Content-Type: application/json" \
  -d '{"amount": 5000, "currency": "USD"}'
```

### See also

- Error Codes
- API Idempotency

---

## Error Codes

Understand and handle errors returned by the Plexy API.

### Error structure

```json
{
  "error": {
    "type": "invalid_request_error",
    "code": "parameter_missing",
    "message": "Missing required parameter: amount",
    "param": "amount",
    "request_id": "req_abc123"
  }
}
```

| Field | Description |
|-------|-------------|
| `type` | Error category |
| `code` | Specific error code |
| `message` | Human-readable description |
| `param` | Parameter that caused error (if applicable) |
| `request_id` | Unique request identifier |

### Error types

#### `api_error`

Server-side errors. Usually temporary - retry with backoff.

| Code | Description |
|------|-------------|
| `api_error` | General API error |

#### `invalid_request_error`

Client-side request errors. Fix the request and retry.

| Code | Description |
|------|-------------|
| `parameter_missing` | Required parameter not provided |
| `parameter_invalid` | Parameter value is invalid |
| `parameter_unknown` | Unrecognized parameter |
| `resource_missing` | Referenced resource doesn't exist |
| `idempotency_error` | Idempotency key reused with different params |

#### `authentication_error`

API key or authentication problems.

| Code | Description |
|------|-------------|
| `api_key_invalid` | Invalid API key |
| `api_key_expired` | API key has expired |
| `api_key_revoked` | API key was revoked |

#### `card_error`

Payment card issues. Display message to customer.

| Code | Description |
|------|-------------|
| `card_declined` | Card was declined |
| `expired_card` | Card has expired |
| `incorrect_cvc` | CVC is incorrect |
| `incorrect_number` | Card number is incorrect |
| `insufficient_funds` | Card has insufficient funds |
| `processing_error` | Error processing card |

### Handling errors

The API always returns an `error` object in the response body for non-2xx responses. Check the HTTP status code first, then inspect `error.type` and `error.code`:

```bash
curl -X POST https://api.plexypay.com/v2/payments \
  -H "x-api-key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"amount": 5000, "currency": "USD", "payment_method": "pm_card_visa"}'
```

Example error response:

```json
{
  "error": {
    "type": "card_error",
    "code": "card_declined",
    "message": "Your card was declined.",
    "request_id": "req_abc123"
  }
}
```

| HTTP status | `error.type` | Action |
|-------------|-------------|--------|
| `402` | `card_error` | Show `error.message` to the customer |
| `400` | `invalid_request_error` | Fix the request parameters |
| `401` | `authentication_error` | Check your API key configuration |
| `500–503` | `api_error` | Retry with exponential backoff |

### Customer-facing messages

Map error codes to user-friendly messages:

| Code | Customer Message |
|------|------------------|
| `card_declined` | "Your card was declined. Please try a different card." |
| `expired_card` | "Your card has expired. Please use a valid card." |
| `incorrect_cvc` | "The security code is incorrect. Please check and try again." |
| `insufficient_funds` | "Your card has insufficient funds. Please try a different card." |
| `processing_error` | "An error occurred processing your card. Please try again." |

### See also

- Refusal Reasons
- HTTP Status Codes

---

## Refusal Reasons

When a payment is declined, the response includes a decline code indicating the reason.

### Common decline codes

| Code                 | Description                          | Recoverable |
| -------------------- | ------------------------------------ | ----------- |
| `generic_decline`    | Card declined for unspecified reason | Maybe       |
| `insufficient_funds` | Not enough funds available           | Yes         |
| `lost_card`          | Card reported lost                   | No          |
| `stolen_card`        | Card reported stolen                 | No          |
| `expired_card`       | Card has expired                     | No          |
| `incorrect_cvc`      | CVC verification failed              | Yes         |
| `incorrect_number`   | Card number is incorrect             | Yes         |
| `processing_error`   | Processing error occurred            | Yes         |
| `do_not_honor`       | Issuer declined without reason       | Maybe       |
| `pickup_card`        | Card should be retained              | No          |

### Decline response

```json
{
  "id": "pay_abc123",
  "status": "failed",
  "failure_code": "insufficient_funds",
  "failure_message": "The card has insufficient funds.",
  "outcome": {
    "type": "issuer_declined",
    "reason": "insufficient_funds",
    "ripr_level": "normal",
    "seller_message": "The card has insufficient funds to complete the purchase."
  }
}
```

### Handling declines

#### Recoverable declines

For recoverable errors, prompt the customer to:

- Use a different payment method
- Enter correct card details
- Contact their bank

```javascript
if (payment.failure_code === 'insufficient_funds') {
  showMessage('Your card has insufficient funds. Please try a different card.');
  showPaymentForm();
}
```

#### Non-recoverable declines

For permanent declines, advise the customer to use a different payment method:

```javascript
if (
  ['lost_card', 'stolen_card', 'pickup_card'].includes(payment.failure_code)
) {
  showMessage(
    'This card cannot be used. Please try a different payment method.',
  );
}
```

### Customer messaging

| Decline Code         | Recommended Message                                                                 |
| -------------------- | ----------------------------------------------------------------------------------- |
| `generic_decline`    | "Your payment was declined. Please try a different card or contact your bank."      |
| `insufficient_funds` | "Your card has insufficient funds. Please try a different card."                    |
| `lost_card`          | "This card cannot be used. Please use a different payment method."                  |
| `expired_card`       | "Your card has expired. Please update your card details."                           |
| `incorrect_cvc`      | "The security code is incorrect. Please check and try again."                       |
| `incorrect_number`   | "The card number is incorrect. Please check and try again."                         |
| `do_not_honor`       | "Your bank declined the payment. Please contact your bank or try a different card." |

### Retry recommendations

| Category           | Action                                  |
| ------------------ | --------------------------------------- |
| Card details wrong | Let customer retry with correct details |
| Insufficient funds | Suggest different card                  |
| Fraud-related      | Don't retry, suggest different method   |
| Bank declined      | Suggest contacting bank                 |

### Reduce declines

1. **Collect complete billing address** - Improves AVS matching
2. **Request CVC** - Reduces fraud declines
3. **Use 3D Secure** - Shifts liability, improves approval
4. **Update stored cards** - Use Account Updater for subscriptions

### See also

- Error Codes

