> ## Documentation Index
> Fetch the complete documentation index at: https://docs.hitpayapp.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Handling Failed Payment Requests

> Learn how to handle failed payment requests and error webhooks for in-person payments

When a payment fails on the terminal (e.g., card declined, transaction timeout, insufficient funds, or other terminal errors), HitPay will send a failed webhook to your webhook endpoint. This webhook indicates that the payment request has failed and requires proper handling in your application.

## Failed Webhook Behavior

When a terminal action fails:

1. **Webhook Triggered**: HitPay automatically sends a webhook to your configured webhook URL with `status=failed`
2. **Payment Request Status Updated**: The payment request status is automatically updated to `failed` in HitPay's system
3. **Error Information**: The webhook includes error details that should be presented to the user

## Failed Webhook Payload

When a payment fails on the terminal, HitPay will send a webhook in one of two formats depending on your webhook configuration:

### Format 1: Form-Encoded (Webhook v1)

If you're using the `webhook` parameter in the Payment Request API (webhook v1), you'll receive a form-encoded payload with `status=failed` and includes an error message:

```
payment_id=&payment_request_id=92965a20-dae5-4d89-a452-5fdfa382dbe1&reference_number=ABC123&phone=&amount=599.00&currency=SGD&status=failed&error_message=Card declined&hmac=330c34a6a8fb9ddb75833620dedb94bf4d4c2e51399d346cbc2b08c381a1399c
```

### Format 2: JSON Event (Webhook Events)

If you're using registered webhook events (as described in [Webhooks documentation](/apis/guide/events)), you'll receive a JSON payload with the `payment_request.failed` event:

<Accordion title="Example JSON Event for payment_request.failed">
  <code>
    ```json theme={"system"}
    {
      "id": "a03e3915-5ec0-44de-a02b-0af213b62b35",
      "name": "Isabel Witting",
      "email": "hitpaydemotest@yopmail.com",
      "phone": "+6565362368",
      "amount": "7.65",
      "currency": "SGD",
      "is_currency_editable": false,
      "status": "failed",
      "purpose": "Purpose_1747900502",
      "reference_number": "1747900502",
      "payment_methods": [
        "wifi_card_reader"
      ],
      "url": "https://securecheckout.staging.hit-pay.com/payment-request/@nit/a03e3915-5ec0-44de-a02b-0af213b62b35/checkout",
      "redirect_url": "https://google.com",
      "webhook": "https://webhook.site/dab6a317-9acb-4939-b907-4924b5e144ec",
      "send_sms": false,
      "send_email": true,
      "sms_status": "pending",
      "email_status": "pending",
      "allow_repeated_payments": false,
      "expiry_date": null,
      "address": {
        "city": "Gotham",
        "line1": "a",
        "state": "N/A",
        "country": "SG",
        "postal_code": "77777"
      },
      "line_items": null,
      "executor_id": null,
      "created_at": "2025-10-31T14:11:01",
      "updated_at": "2025-10-31T14:11:13",
      "staff_id": null,
      "business_location_id": null,
      "location": null,
      "payments": [
        {
          "id": "a03e3915-eae4-4a0f-9357-6d7869f80ea2",
          "quantity": 1,
          "status": "failed",
          "status_reason_code": "withdrawal_count_limit_exceeded",
          "status_reason": "Withdrawal or limit exceeded. Please use another card.",
          "buyer_name": "Isabel Witting",
          "buyer_phone": "+6565362368",
          "buyer_email": "hitpaydemotest@yopmail.com",
          "currency": "sgd",
          "amount": "7.65",
          "refunded_amount": "0.00",
          "payment_type": "card_present",
          "fees": "0.24",
          "fees_currency": null,
          "created_at": "2025-10-31T14:11:01",
          "updated_at": "2025-10-31T14:11:12"
        }
      ]
    }
    ```
  </code>
</Accordion>

**Key Fields in JSON Event Format:**

* `status`: Will be `"failed"` for failed payment requests
* `payments[].status`: Will be `"failed"` for failed payments
* `payments[].status_reason`: Human-readable error message explaining the failure
* `payments[].status_reason_code`: Machine-readable error code (e.g., `"withdrawal_count_limit_exceeded"`)

**Webhook Headers for JSON Events:**
When receiving JSON event webhooks, check these headers:

* `Hitpay-Event-Type`: Will be `"failed"` for failed events
* `Hitpay-Event-Object`: Will be `"payment_request"` for payment request events
* `Hitpay-Signature`: SHA-256 HMAC signature for validation (see [Webhook Validation](/apis/guide/events#validating-webhook))

## Implementation Guidelines

To properly handle failed payment requests in your application, you need to handle both webhook formats:

### Handling Form-Encoded Webhooks (Webhook v1)

<CodeGroup>
  ```php PHP theme={"system"}
  // In your webhook handler
  $payload = $_POST; // Form-encoded payload

  if ($payload['status'] === 'failed') {
      // Handle failed payment
      $errorMessage = $payload['error_message'] ?? 'Payment failed';
      $paymentRequestId = $payload['payment_request_id'];
      
      // Update your order/payment status
      updateOrderStatus($paymentRequestId, 'failed');
      
      // Notify your client/UI about the failure
      notifyClientFailure($paymentRequestId, $errorMessage);
      
      // Display error to user
      // You should show the error_message to the user
  }
  ```

  ```javascript JavaScript theme={"system"}
  // In your webhook handler (Node.js example)
  app.post('/webhook', express.urlencoded({ extended: true }), (req, res) => {
    const payload = req.body; // Form-encoded payload
    
    if (payload.status === 'failed') {
      const errorMessage = payload.error_message || 'Payment failed';
      const paymentRequestId = payload.payment_request_id;
      
      // Update your order/payment status
      updateOrderStatus(paymentRequestId, 'failed');
      
      // Notify your client/UI about the failure
      notifyClientFailure(paymentRequestId, errorMessage);
      
      // Display error to user
      // You should show the error_message to the user
    }
    
    res.status(200).send('OK');
  });
  ```
</CodeGroup>

### Handling JSON Event Webhooks

<CodeGroup>
  ```php PHP theme={"system"}
  // In your webhook handler for JSON events
  $headers = getallheaders();
  $eventType = $headers['Hitpay-Event-Type'] ?? '';
  $eventObject = $headers['Hitpay-Event-Object'] ?? '';
  $payload = json_decode(file_get_contents('php://input'), true);

  // Check if this is a payment_request.failed event
  if ($eventObject === 'payment_request' && $eventType === 'failed') {
      // Extract error message from payments array
      $errorMessage = 'Payment failed';
      if (!empty($payload['payments']) && is_array($payload['payments'])) {
          $failedPayment = $payload['payments'][0];
          $errorMessage = $failedPayment['status_reason'] ?? $failedPayment['status_reason_code'] ?? 'Payment failed';
      }
      
      $paymentRequestId = $payload['id'];
      
      // Update your order/payment status
      updateOrderStatus($paymentRequestId, 'failed');
      
      // Notify your client/UI about the failure
      notifyClientFailure($paymentRequestId, $errorMessage);
      
      // Display error to user
      // You should show the status_reason to the user
  }
  ```

  ```javascript JavaScript theme={"system"}
  // In your webhook handler for JSON events (Node.js example)
  app.post('/webhook', express.json(), (req, res) => {
    const eventType = req.headers['hitpay-event-type'];
    const eventObject = req.headers['hitpay-event-object'];
    const payload = req.body; // JSON payload
    
    // Check if this is a payment_request.failed event
    if (eventObject === 'payment_request' && eventType === 'failed') {
      // Extract error message from payments array
      let errorMessage = 'Payment failed';
      if (payload.payments && payload.payments.length > 0) {
        const failedPayment = payload.payments[0];
        errorMessage = failedPayment.status_reason || failedPayment.status_reason_code || 'Payment failed';
      }
      
      const paymentRequestId = payload.id;
      
      // Update your order/payment status
      updateOrderStatus(paymentRequestId, 'failed');
      
      // Notify your client/UI about the failure
      notifyClientFailure(paymentRequestId, errorMessage);
      
      // Display error to user
      // You should show the status_reason to the user
    }
    
    res.status(200).send('OK');
  });
  ```
</CodeGroup>

## Displaying Error Messages

Always present the error message from the webhook to your users. This helps them understand why the payment failed:

* **For Form-Encoded Webhooks**: Use the `error_message` field
* **For JSON Event Webhooks**: Use the `payments[].status_reason` field (or `status_reason_code` as fallback)

Common error messages you may encounter:

* **Card Declined**: "Your card was declined. Please try a different payment method."
* **Insufficient Funds**: "Insufficient funds. Please check your account balance."
* **Timeout**: "Payment processing timed out. Please try again."
* **Terminal Error**: "Terminal error occurred. Please contact staff."
* **Limit Exceeded**: "Withdrawal or limit exceeded. Please use another card."

## Best Practices

1. **Update Payment Request Status**: Mark the payment request as failed in your system when you receive the failed webhook.

2. **User Experience**:
   * Display a clear error message to the user on your POS/kiosk interface
   * Optionally allow the user to retry the payment
   * Update any pending transaction indicators

3. **Security**: Always validate webhooks using HMAC verification before processing failed status updates. Never trust unverified webhook payloads.

## Common Failure Scenarios

Failed webhooks can be triggered by various terminal errors:

* **Card Declined**: Customer's card was declined by the bank
* **Insufficient Funds**: Customer's account doesn't have enough balance
* **Transaction Timeout**: Payment took too long to process
* **Card Reader Error**: Physical issue with the card reader
* **Network Issues**: Terminal lost connection during payment
* **Cancelled by User**: Customer cancelled the payment on the terminal
* **Invalid Card**: Card details are invalid or expired

## Important Notes

<Warning>
  Always validate webhooks using HMAC verification before processing failed status updates. Never trust unverified webhook payloads.
</Warning>

<Info>
  The failed webhook is sent automatically by HitPay whenever a terminal action fails. You do not need to poll the API for payment status - simply listen to the webhook and handle the response accordingly.
</Info>

## Related Documentation

* [In-Person Payments Overview](/apis/guide/in-person-payments/overview) - Learn the basics of in-person payments
* [Testing in Sandbox](/apis/guide/in-person-payments/testing) - Test failed payment scenarios in Sandbox
* [Webhook Validation](/apis/guide/events#validating-webhook) - Learn how to validate webhook signatures
