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

# Webhooks

> Receive real-time updates about the status of your transactions.

# Webhooks

The **Webhook** feature allows your system to receive real-time updates about the status of transactions. When a transaction status changes, Tumipay sends a `POST` request to the URL provided in the `ipn_url` field during the transaction creation.

***

## Webhook Payload

The webhook payload is a JSON object containing the following information:

```json theme={null}
{
  "top_status": "APPROVED",
  "top_ticket": "49e3c70f-49d2-11ef-a534-02530a7dec0f",
  "top_reference": "ef3bc5cc-1a08-41c8-9e3b-449b95ac5eb6",
  "top_amount": 20000,
  "top_currency": "COP",
  "top_payment_method": "RECAUDO",
  "top_bank_name": "BANCO FALABELLA",
  "top_response_message": "Transacción exitosa",
  "top_user": {
    "legal_doc_type": "CC",
    "legal_doc": "879406421",
    "phone_code": "57",
    "phone_number": "3002134607",
    "email": "janejohnson@email.com",
    "full_name": "Jane Johnson"
  },
  "top_card": {
    "bank": "BANCO FALABELLA",
    "bin": "531122",
    "country": "Colombia",
    "lastFourDigits": "1974",
    "type": "credit",
    "brand": "Mastercard"
  },
  "top_user_email": "janejohnson@email.com",
  "top_user_phone": "3002134607",
  "top_extra_params1": ""
}
```

**Payload Properties:**

* **top\_status**: The status of the transaction. Possible values:
  * `APPROVED`: The transaction was successfully completed.
  * `REJECTED`: The transaction failed during the normal flow (e.g., declined by the bank).
  * `DECLINED`: The transaction was canceled due to internal rules, such as fraud detection, transactional limits, or additional configurations by the merchant.
  * `PENDING`: The transaction is in progress and awaiting confirmation.
* **top\_ticket**: Unique identifier of the transaction.
* **top\_reference**: Reference identifier provided by the merchant for the transaction.
* **top\_amount**: Total amount of the transaction.
* **top\_currency**: Currency of the transaction (e.g., COP).
* **top\_payment\_method**: The payment method used (e.g., RECAUDO).
* **top\_bank\_name**: Name of the bank involved in the transaction.
* **top\_response\_message**: Message describing the transaction’s outcome.
* **top\_user**: An object containing detailed information about the user/customer:
  * **legal\_doc\_type**: Type of the customer legal document (e.g., CC).
  * **legal\_doc**: Customer legal document number.
  * **phone\_code**: Customer country phone code.
  * **phone\_number**: Customer phone number.
  * **email**: Customer email address.
  * **full\_name**: Customer full name.
* **top\_user\_email**: Customer email address (repeated for convenience).
* **top\_user\_phone**: Customer phone number (repeated for convenience).
* **top\_card**: An object containing information about the card used in the transaction (if applicable):
  * **bank**: Name of the issuing bank.
  * **bin**: Bank Identification Number (first 6 digits of the card).
  * **country**: Country of issuance.
  * **lastFourDigits**: Last four digits of the card number.
  * **type**: Card type (e.g., credit, debit).
  * **brand**: Card brand (e.g., Visa, Mastercard).
* **top\_extra\_params1**: Additional parameters.

***

**Webhook Headers**

The webhook request includes the following headers for validation:

* **x-trx-signature**: A signature generated based on certain values in the payload.
* **User-Agent**: The user agent for the webhook is `Tumipay/1.1`.

**Signature Generation**

The signature is generated using the SHA256 algorithm by hashing a JSON object that includes the customer token, the transaction ticket, and the transaction reference. Here’s the format:

```json theme={null}
sha256('{
    "token": "<client_token>",
    "ticket": "<uuid_of_transaction>",
    "reference": "<transaction_reference>"
}');
```

Make sure your system validates the signature for security purposes.

<CodeGroup dropdown>
  ```php Php theme={null}
  function valid_webhook(array $body, string $signature, string $clientToken): bool {
      $message = json_encode([
          'token' => $clientToken,
          'ticket' => $body['top_ticket'],
          'reference' => $body['top_reference'],
      ]);
      $expected = hash('sha256', $message);
      return hash_equals($expected, $signature);
  }
  ```

  ```go Go theme={null}
  import (
      "crypto/sha256"
      "encoding/hex"
      "encoding/json"
  )

  func validWebhook(body map[string]string, signature, clientToken string) bool {
      msg, _ := json.Marshal(map[string]string{
          "token":     clientToken,
          "ticket":    body["top_ticket"],
          "reference": body["top_reference"],
      })
      sum := sha256.Sum256(msg)
      return hex.EncodeToString(sum[:]) == signature
  }
  ```

  ```java Java theme={null}
  import com.fasterxml.jackson.databind.ObjectMapper;
  import java.math.BigInteger;
  import java.nio.charset.StandardCharsets;
  import java.security.MessageDigest;
  import java.util.Map;

  boolean validWebhook(Map<String, String> body, String signature, String clientToken) throws Exception {
      ObjectMapper mapper = new ObjectMapper();
      String message = mapper.writeValueAsString(Map.of(
          "token", clientToken,
          "ticket", body.get("top_ticket"),
          "reference", body.get("top_reference")
      ));
      MessageDigest md = MessageDigest.getInstance("SHA-256");
      byte[] digest = md.digest(message.getBytes(StandardCharsets.UTF_8));
      String expected = String.format("%064x", new BigInteger(1, digest));
      return expected.equals(signature);
  }
  ```

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

  def valid_webhook(body, signature, client_token):
      message = json.dumps({
          "token": client_token,
          "ticket": body["top_ticket"],
          "reference": body["top_reference"],
      }, separators=(",", ":")).encode()
      expected = hashlib.sha256(message).hexdigest()
      return expected == signature
  ```

  ```javascript Javascript theme={null}
  const crypto = require("crypto");

  function validWebhook(body, signature, clientToken) {
    const message = JSON.stringify({
      token: clientToken,
      ticket: body.top_ticket,
      reference: body.top_reference,
    });
    const expected = crypto.createHash("sha256").update(message).digest("hex");
    return expected === signature;
  }
  ```
</CodeGroup>

***

#### Handling Webhook Events

Your system should respond to webhook requests with a `200 OK` HTTP status code to confirm successful receipt. If Tumipay does not receive this response, it will automatically retry the webhook delivery up to three times.

**Example Use Case: Declined vs Rejected**

* **DECLINED**: Indicates the transaction was canceled internally by merchant-specific rules, such as:
  * Fraud detection triggers.
  * Exceeding transactional limits.
  * Additional configurations made by the merchant.
* **REJECTED**: Indicates the transaction was declined during the normal transaction process, such as:
  * Insufficient funds.
  * Bank-specific rejections.

### Example Use Case: Idempotent Processing

To prevent processing the same webhook event multiple times:

* Store a record of processed transactions using their `top_ticket` or `top_reference` as a unique key.
* When a webhook is received, check if the `top_ticket` already exists in your database:
  * If it exists, skip processing.
  * If it does not exist, proceed with handling the event and store the record.

By following these practices, you can ensure a reliable and secure webhook implementation that effectively handles real-time transaction updates.

***
