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

# Webhooks: Real-time Payment Notifications

> Set up Hello Clever webhooks to receive POST notifications on payment status changes. Includes payload reference, error codes, security, and retry behaviour.

Hello Clever webhooks let your server receive real-time notifications whenever a payment’s status changes (for example, when a payment moves from `pending` to `authorised`, or from `authorised` to `waiting`). Instead of polling the API, you register an endpoint and Hello Clever sends a POST request to it automatically on each status transition.

## How webhooks work

When a payment status changes, Hello Clever sends a `POST` request to your configured `endpoint_url` with a JSON payload describing the current state of the transaction. Your endpoint must return HTTP `200` to acknowledge receipt. If it does not, Hello Clever retries up to 3 times with a 15-minute delay between each attempt.

<Note>
  Because retries can deliver the same payload more than once, implement idempotent handling in your webhook endpoint. Use the `uuid` field to deduplicate events.
</Note>

## Setting up webhooks

You can configure your webhook endpoint in two ways:

**SDK integration**: Contact Hello Clever to register your default webhook URL. Any payment created via the SDK will send notifications to that URL.

**API integration**: Include a `webhook_notification` object in your payment creation request. If provided, it overrides the default webhook URL for that payment.

```json theme={null}
{
  "webhook_notification": {
    "endpoint_url": "https://your-domain.com/webhook",
    "authorization_header": "Bearer your-secret-token"
  }
}
```

<ResponseField name="webhook_notification" type="object">
  Webhook configuration to include in a payment creation request.

  <Expandable>
    <ResponseField name="endpoint_url" type="string" required>
      The URL where Hello Clever will POST webhook notifications.
    </ResponseField>

    <ResponseField name="authorization_header" type="string">
      Optional. A value Hello Clever will send in the `Authorization` header of every webhook request to this endpoint, allowing you to verify the source.
    </ResponseField>
  </Expandable>
</ResponseField>

## Payment statuses

Hello Clever sends a webhook notification whenever a payment transitions to any of the following statuses:

| Status               | Description                                                                                         |
| -------------------- | --------------------------------------------------------------------------------------------------- |
| `pending`            | Customer started a new payment but has not proceeded yet.                                           |
| `authorised`         | Payment has been authorised and is ready to be captured.                                            |
| `waiting`            | Payment has been approved; awaiting funds to settle.                                                |
| `received`           | Funds have been received.                                                                           |
| `expired`            | Payment session expired before completion.                                                          |
| `return_pending`     | Refund request initiated, awaiting processing.                                                      |
| `return_expired`     | Refund request expired (expiration window is 10 days).                                              |
| `partially_refunded` | A partial refund has been issued to the customer.                                                   |
| `return_received`    | Full amount has been refunded to the customer.                                                      |
| `return_rejected`    | Refund request was denied. The system will not retry.                                               |
| `failed`             | Payment failed due to an error or decline.                                                          |
| `in_dispute`         | Customer has raised a dispute; it is under review.                                                  |
| `dispute_lost`       | Dispute was resolved in the customer’s favour. The disputed amount is not returned to the merchant. |

<Note>
  When a payment is in `in_dispute` status, contact Hello Clever support to submit evidence for the dispute resolution process.
</Note>

## Webhook object

Each webhook POST body contains the following fields:

| Field                                       | Type    | Description                                                                                                   |
| ------------------------------------------- | ------- | ------------------------------------------------------------------------------------------------------------- |
| `uuid`                                      | string  | Unique identifier for the payment transaction.                                                                |
| `name`                                      | string  | Customer’s name.                                                                                              |
| `email`                                     | string  | Customer’s email address.                                                                                     |
| `external_id`                               | string  | Your reference ID for this payment.                                                                           |
| `status`                                    | string  | Current status of the payment.                                                                                |
| `pay_code`                                  | object  | When 3DS is required, contains `3ds_url`. When the payment failed, contains `error_code` and `error_message`. |
| `pay_code.3ds_url`                          | string  | URL for the customer to complete 3DS authentication (when required).                                          |
| `pay_code.error_code`                       | string  | Error or decline code (e.g. `issuer_declined`).                                                               |
| `pay_code.error_message`                    | string  | Human-readable error or decline message.                                                                      |
| `currency`                                  | string  | Transaction currency code (ISO 4217).                                                                         |
| `amount`                                    | string  | Original payment amount.                                                                                      |
| `total`                                     | string  | Total amount including any fees.                                                                              |
| `paid_amount`                               | string  | Amount that has been paid.                                                                                    |
| `is_refundable`                             | boolean | Whether the payment can be refunded.                                                                          |
| `payment_method`                            | string  | Payment method used (e.g. `card`).                                                                            |
| `expired_at`                                | string  | Timestamp indicating when the payment URL expires (UTC).                                                      |
| `webhook_notification`                      | object  | Webhook configuration details.                                                                                |
| `webhook_notification.endpoint_url`         | string  | URL where webhooks are sent.                                                                                  |
| `webhook_notification.authorization_header` | string  | Masked authorization header value.                                                                            |
| `refund_information`                        | object  | Details about any refunds.                                                                                    |
| `refund_information.total_amount`           | string  | Total amount available for refund.                                                                            |
| `refund_information.refund_amount`          | string  | Amount that was refunded.                                                                                     |
| `refund_information.description`            | string  | Reason for the refund.                                                                                        |
| `sender_details`                            | object  | Payment method details.                                                                                       |
| `sender_details.card`                       | object  | Card payment details.                                                                                         |
| `sender_details.card.card_type`             | string  | Type of card used.                                                                                            |
| `sender_details.card.card_brand`            | string  | Card brand (e.g. `visa`, `mastercard`).                                                                       |
| `sender_details.card.card_last_4`           | string  | Last 4 digits of the card number.                                                                             |
| `created_at`                                | string  | Timestamp when the payment was created.                                                                       |
| `token`                                     | object  | Card token details (only present when status is `authorised` or `waiting`).                                   |
| `token.id`                                  | string  | Reusable token for the card (format: `tok_...`).                                                              |
| `token.type`                                | string  | Token type (e.g. `card`).                                                                                     |

### Example payload

```json theme={null}
{
  "uuid": "QVABPPC7",
  "name": "Hello Clever",
  "email": "test@example.com",
  "external_id": "123",
  "status": "authorised",
  "pay_code": {
    "3ds_url": "https://3ds-example.com"
  },
  "currency": "USD",
  "amount": "100.0",
  "total": "100.0",
  "paid_amount": "0.0",
  "is_refundable": false,
  "payment_method": "card",
  "expired_at": "",
  "webhook_notification": {
    "endpoint_url": "https://webhook.site/12da7803-c4cf-4f32-812d-aaeaecf20d9d",
    "authorization_header": "****"
  },
  "refund_information": {
    "total_amount": "250.0",
    "refund_amount": "10.0",
    "description": "Testing refund"
  },
  "sender_details": {
    "card": {
      "card_type": "card",
      "card_brand": "visa",
      "card_last_4": "1111",
      "card_country": "US"
    }
  },
  "created_at": "2025-05-30T05:11:17.602+0000",
  "token": {
    "id": "tok_dfe1988a1ffc0d6562d3",
    "type": "card"
  }
}
```

## Token for authorised cards

When a card payment reaches `authorised` or `waiting` status, Hello Clever includes a `token` object in the webhook payload. Save this token securely: you can use it to create future payments for the same customer without requiring them to re-enter their card details.

```json theme={null}
{
  "token": {
    "id": "tok_773085396b86562040f4",
    "type": "card"
  }
}
```

<Warning>
  The `token` object is only present in webhook notifications when the status changes to `authorised` or `waiting`. It is not included in notifications for any other status.
</Warning>

## Webhook security

Hello Clever supports two security mechanisms you can use independently or together.

### Authorization header

If you set an `authorization_header` in your webhook configuration, Hello Clever will send that value in the `Authorization` header of every webhook request to your endpoint:

```
Authorization: Bearer your-secret-token
```

Verify this header on your server against your known secret to confirm the request is from Hello Clever.

### HTTP webhook signature (HMAC-SHA256)

Hello Clever also signs each webhook payload using HMAC-SHA256 and sends the signature in the `HTTP-WEBHOOK-SIGNATURE` header:

```
HTTP-WEBHOOK-SIGNATURE: ff456ed178c487a2fd323043315320f8ec0908327a1b76f1ad54d9f81a000f9d
```

To verify the signature, recompute the HMAC-SHA256 of the **raw request body** using your Webhook Secret Key and compare it with the header value.

<Steps>
  <Step title="Find your Webhook Secret Key">
    In the Merchant Portal, go to **Dashboard → Developer → Authentication → Webhook tab** and copy the value from the **Webhook secret key** field.
  </Step>

  <Step title="Compute the expected signature">
    Use the raw request body (do not parse and re-stringify JSON) and your Webhook Secret Key:

    <CodeGroup>
      ```javascript Node.js theme={null}
      const crypto = require('crypto');

      function verifySignature(rawBody, webhookSecretKey, receivedSignature) {
        const expectedSignature = crypto
          .createHmac('sha256', webhookSecretKey)
          .update(rawBody, 'utf8')
          .digest('hex');
        return receivedSignature === expectedSignature;
      }

      // In your request handler:
      const receivedSignature = req.headers['http-webhook-signature'];
      const isValid = verifySignature(req.rawBody, process.env.WEBHOOK_SECRET, receivedSignature);
      ```

      ```python Python theme={null}
      import hmac
      import hashlib

      def verify_signature(raw_body: str, webhook_secret_key: str, received_signature: str) -> bool:
          expected_signature = hmac.new(
              webhook_secret_key.encode('utf-8'),
              raw_body.encode('utf-8'),
              hashlib.sha256
          ).hexdigest()
          return received_signature == expected_signature

      # In your request handler:
      received_signature = request.headers.get('HTTP-WEBHOOK-SIGNATURE')
      is_valid = verify_signature(request.get_data(as_text=True), WEBHOOK_SECRET, received_signature)
      ```

      ```java Java theme={null}
      import javax.crypto.Mac;
      import javax.crypto.spec.SecretKeySpec;
      import java.nio.charset.StandardCharsets;

      public boolean verifySignature(String rawBody, String secret, String receivedSignature) throws Exception {
          Mac sha256_HMAC = Mac.getInstance("HmacSHA256");
          SecretKeySpec secretKey = new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256");
          sha256_HMAC.init(secretKey);
          byte[] hash = sha256_HMAC.doFinal(rawBody.getBytes(StandardCharsets.UTF_8));
          StringBuilder hexString = new StringBuilder();
          for (byte b : hash) {
              String hex = Integer.toHexString(0xff & b);
              if (hex.length() == 1) hexString.append('0');
              hexString.append(hex);
          }
          return hexString.toString().equals(receivedSignature);
      }
      ```
    </CodeGroup>
  </Step>

  <Step title="Compare and accept or reject">
    If the signatures match, the payload is authentic and unmodified. If they differ, reject the request; it may have been tampered with or sent by an untrusted source.
  </Step>
</Steps>

<Tip>
  For maximum security, validate both the `Authorization` header and the `HTTP-WEBHOOK-SIGNATURE`. The `Authorization` header confirms the sender; the signature confirms the payload has not been tampered with.
</Tip>

## Error handling and retries

If your endpoint does not return HTTP `200`, Hello Clever retries the webhook call up to **3 times** with a **15-minute delay** between each attempt.

To handle retries correctly:

* Return `200` immediately upon receiving the webhook, before performing any heavy processing.
* Process the event asynchronously (e.g. via a queue) to avoid timeouts.
* Use the `uuid` field to detect and discard duplicate deliveries.

## Error codes

When a payment fails, the `pay_code` object includes `error_code` and `error_message`. The following table lists all possible error codes:

| Error code                         | Message                                                                                            |
| ---------------------------------- | -------------------------------------------------------------------------------------------------- |
| `account_closed`                   | The customer’s bank account has been closed.                                                       |
| `amount_invalid`                   | The payment amount is invalid, or exceeds the amount that’s allowed.                               |
| `amount_too_large`                 | The specified amount is greater than the maximum amount allowed. Use a lower amount and try again. |
| `amount_too_small`                 | The specified amount is less than the minimum amount allowed. Use a higher amount and try again.   |
| `authentication_expired`           | The card authorisation has expired.                                                                |
| `authentication_failed`            | The payment can’t be authorised.                                                                   |
| `authentication_required`          | The card was declined because the transaction requires authentication such as 3D Secure.           |
| `capture_not_authorised`           | Transaction must be in `authorised` status before it can be captured.                              |
| `card_decline_rate_limit_exceeded` | This card has been declined too many times. You can try to charge this card again after 24 hours.  |
| `card_expired`                     | The card has expired.                                                                              |
| `card_lost`                        | The payment was declined because the card is reported lost.                                        |
| `card_not_supported`               | The card does not support this type of purchase.                                                   |
| `card_number_incorrect`            | The card number is incorrect.                                                                      |
| `card_restricted`                  | The customer can’t use this card to make this payment.                                             |
| `card_stolen`                      | The payment was declined because the card is reported stolen.                                      |
| `card_velocity_exceeded`           | The customer has exceeded the balance, credit limit, or transaction amount limit on their card.    |
| `currency_not_supported`           | The card does not support the specified currency.                                                  |
| `customer_canceled`                | The customer has stopped the payment with their bank.                                              |
| `cvc_incorrect`                    | The CVC number is incorrect.                                                                       |
| `debit_not_authorised`             | The customer has notified their bank that this payment was unauthorised.                           |
| `expiry_month_invalid`             | The expiration month is invalid.                                                                   |
| `expiry_year_invalid`              | The expiration year is invalid.                                                                    |
| `fraudulent`                       | The payment was declined because it is suspected to be fraudulent.                                 |
| `generic_declined`                 | The card was declined for an unknown reason or the payment was blocked.                            |
| `insufficient_funds`               | The card has insufficient funds to complete the purchase.                                          |
| `issuer_declined`                  | The card was declined for an unknown reason.                                                       |
| `issuer_not_available`             | The card issuer couldn’t be reached, so the payment couldn’t be authorised.                        |
| `pin_required`                     | The card was declined because it requires a PIN.                                                   |
| `pin_try_exceeded`                 | The allowable number of PIN tries was exceeded.                                                    |
| `processing_error`                 | An error occurred while processing the card. Try again later or with a different payment method.   |
| `return_url_invalid`               | Return URL is invalid.                                                                             |
| `transaction_is_blocked`           | Transaction is blocked. If you paid, a refund will be issued.                                      |
| `transaction_duplicated`           | A transaction with identical details was submitted recently.                                       |
| `withdrawal_count_limit_exceeded`  | The customer has exceeded the balance or credit limit available on their card.                     |

## Best practices

1. Verify the `Authorization` header or `HTTP-WEBHOOK-SIGNATURE` on every incoming request before processing.
2. Implement idempotency using the `uuid` field to prevent duplicate side effects from retried deliveries.
3. Return `200 OK` as soon as you receive the request; process the payload asynchronously.
4. Keep your webhook endpoint URL private; avoid sharing it publicly.
5. Use HTTPS for your webhook endpoint to protect the payload in transit.
6. Test your endpoint during development using tools like [webhook.site](https://webhook.site) or a local tunnel such as [ngrok](https://ngrok.com).


## Related topics

- [How it works](/getting-started/how-it-works.md)
- [Real-time Payin Service](/services/real-time-payin.md)
- [Real-time Payments vs. Card Payments](/platform-overview/payment-concepts/rtp-vs-cards.md)
