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

# Webhooks

> Stay updated with real-time notifications via our Webhooks service.

## Overview

Webhooks instantly inform your application about significant events, such as payroll submissions or updates, without the need for polling.

### Use Cases:

* Alerting when users submit or create payroll data. This use case can be handled by subscribing for event of type `user-payroll-submitted`
* Notifying upon successful account linkages. Not yet supported as event type.
* Triggering actions after data updates. Not yet supported as event type.

Webhooks are system-wide meaning subscriptions are not limited to specific users or accounts. For example, if any user updates their payroll with the `user-payroll-submitted` subscription enabled, a webbook will be delivered with the respective user information enclosed.

### Quick Reference:

* **Webhooks List**: Detailed descriptions of all events you can subscribe to.
* **Setup Guide**: Instructions for subscribing and managing webhooks.

### Supported Event:

* `user-payroll-submitted` - notifies when any user creates an entry with new payroll data.
  Includes the payroll data entry as json if `include_payload` is set in the configuration.
* `user-bank-statement-submitted` - notifies when any user uploads bank statements that are
  successfully extracted. Includes bank statement submissions as json if `include_payload` is set
  in the configuration.

## Setting Up Webhooks

Create a webhook by sending a POST request. Specify which events to monitor and where to send notifications.

```bash theme={null}
curl --request POST \
  --url https://api.sandbox.goteal.co/webhooks \
  --header 'Content-Type: application/json' \
  --header 'X-API-KEY: <api-key>' \
  --data '{
    "events": ["user-payroll-submitted", "user-payroll-created"],
    "name": "user-payroll-submitted",
    "signing_secret": "very-secret",
    "encryption_key": "St/73LZ1xHkwX5TanL7V+YAVczn4acozPf3cFySKQXI=",
    "url": "https://webhooks.site"
  }'
```

* **events**: Choose events like `user-payroll-submitted` or `user-bank-statement-submitted` for targeted notifications.
* **name**: Label your webhook for easier management.
* **url**: Your endpoint to receive webhook payloads.
* **signing\_secret**: (Optional) A secret for secure webhook verification.
* **encryption\_key**: (Optional) A Base64-encoded 32-byte key used to encrypt webhook payloads.

### Verifying Webhooks

If the optional `signing_secret` was provided in the webhook subscription, it can be used to validate incoming webhook
signatures, ensuring data integrity and authenticity.

When encryption is enabled, the message is first encrypted and then signed.
Therefore, the signature should be verified against the encrypted message,
not the decrypted content.

To verify if a received webhook event is authentic:

* Encode the webook payload with the `signing_secret` using `HMAC-SHA512`. You can then check that the result of this encoding matches the contents in the
  `X-Teal-Signature` header. If it matches then the webhook received is genuine. Example verification:

If your `signing_secret` was `mysecret` and you received the following webhook payload:

```json theme={null}
{
    "event": "user-payroll-submitted",
    "user_id": "4708334c-70b3-437d-8e46-91ff5c9a8d7d",
    "timestamp": "2024-04-04T12:00:00.00Z"
}
```

The signature to check against would be:

```
"X-Teal-Signature" : "a30540779107a19069257432b775b74b16b32214616638fae2e6027a41a3f2dfb08f44daf3862c335d08fb83501fc769f73d49a1cb137f96f31c6a7db412c197"
```

Example of verifying signature:

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

  // The signature as "X-Teal-Signature" header
  const receivedTealSignature = '...'; 
  const client_secret = 'my little secret';
  const requestBody = '...'; // Your request body here

  // Make sure to use the raw requestBody when validating the signature.
  var signature = crypto
    .createHmac('sha512', client_secret)
    .update(requestBody)
    .digest('hex');

  if (receivedTealSignature === signature) {
      console.log('Signature is valid');
  } else {
      console.log('Signature is invalid');
  }
  ```

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

  receivedTealSignature = '...'  # The signature as "X-Teal-Signature" header
  client_secret = 'my little secret'
  requestBody = '...'  # Your request body here

  # Make sure to use the raw requestBody when validating the signature.
  signature = hmac.new(
    client_secret.encode(),
    msg=requestBody.encode(),
    digestmod=hashlib.sha512
  ).hexdigest()

  if receivedTealSignature == signature:
      print('Signature is valid')
  else:
      print('Signature is invalid')
  ```

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

  public class Main {
      public static void main(String[] args) throws Exception {

          // The signature as "X-Teal-Signature" header
          String receivedTealSignature = "...";
          String client_secret = "my little secret";
          String requestBody = "...";  // Your request body here

          // Make sure to use the raw requestBody when validating the signature.
          Mac sha512_HMAC = Mac.getInstance("HmacSHA512");
          SecretKeySpec secret_key = new SecretKeySpec(
            client_secret.getBytes(StandardCharsets.UTF_8),
            "HmacSHA512"
          );
          sha512_HMAC.init(secret_key);

          byte[] hash = sha512_HMAC
          .doFinal(requestBody.getBytes(StandardCharsets.UTF_8));
          StringBuilder sb = new StringBuilder(hash.length * 2);
          for(byte b : hash) {
              sb.append(String.format("%02x", b));
          }
          String signature = sb.toString();

          if (receivedTealSignature.equals(signature)) {
              System.out.println("Signature is valid");
          } else {
              System.out.println("Signature is invalid");
          }
      }
  }
  ```

  ```kotlin Kotlin theme={null}
  import javax.crypto.Mac
  import javax.crypto.spec.SecretKeySpec
  import java.nio.charset.StandardCharsets
  import java.security.InvalidKeyException
  import java.security.NoSuchAlgorithmException

   // The signature as "X-Teal-Signature" header
  val receivedTealSignature = "..." 
  val client_secret = "my little secret"
  val requestBody = "..."  // Your request body here

  // Make sure to use the raw requestBody when validating the signature.
  val sha512_HMAC = Mac.getInstance("HmacSHA512")
  val secret_key = SecretKeySpec(
    client_secret.toByteArray(StandardCharsets.UTF_8),
    "HmacSHA512"
  )
  sha512_HMAC.init(secret_key)

  val hash = sha512_HMAC
      .doFinal(requestBody.toByteArray(StandardCharsets.UTF_8))
  val signature = hash.joinToString("") { "%02x".format(it) }

  if (receivedTealSignature == signature) {
      println("Signature is valid")
  } else {
      println("Signature is invalid")
  }

  ```
</CodeGroup>

### Generating Encryption Keys

If you want to use encrypted webhooks, you'll need to provide a 32-byte encryption key encoded in Base64 when creating your webhook. Here's how to generate a secure random key in different languages:

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

  // Generate a random 32-byte key
  const encryptionKey = crypto.randomBytes(32);

  // Encode the key as Base64 for use with the API
  const encryptionKeyBase64 = encryptionKey.toString('base64');
  console.log('Encryption Key:', encryptionKeyBase64);
  // Example output: St/73LZ1xHkwX5TanL7V+YAVczn4acozPf3cFySKQXI=
  ```

  ```python Python theme={null}
  import os
  import base64

  # Generate a random 32-byte key
  encryption_key = os.urandom(32)

  # Encode the key as Base64 for use with the API
  encryption_key_base64 = base64.b64encode(encryption_key).decode('utf-8')
  print(f"Encryption Key: {encryption_key_base64}")
  # Example output: St/73LZ1xHkwX5TanL7V+YAVczn4acozPf3cFySKQXI=
  ```

  ```java Java theme={null}
  import java.security.SecureRandom;
  import java.util.Base64;

  public class GenerateEncryptionKey {
      public static void main(String[] args) {
          // Generate a random 32-byte key
          SecureRandom secureRandom = new SecureRandom();
          byte[] key = new byte[32];
          secureRandom.nextBytes(key);
          
          // Encode the key as Base64 for use with the API
          String encryptionKeyBase64 = Base64.getEncoder().encodeToString(key);
          System.out.println("Encryption Key: " + encryptionKeyBase64);
          // Example output: St/73LZ1xHkwX5TanL7V+YAVczn4acozPf3cFySKQXI=
      }
  }
  ```

  ```kotlin Kotlin theme={null}
  import java.security.SecureRandom
  import java.util.Base64

  // Generate a random 32-byte key
  val secureRandom = SecureRandom()
  val key = ByteArray(32)
  secureRandom.nextBytes(key)

  // Encode the key as Base64 for use with the API
  val encryptionKeyBase64 = Base64.getEncoder().encodeToString(key)
  println("Encryption Key: $encryptionKeyBase64")
  // Example output: St/73LZ1xHkwX5TanL7V+YAVczn4acozPf3cFySKQXI=
  ```
</CodeGroup>

### Decrypting Webhook Payloads

When you provide an `encryption_key`, the webhook payload will be encrypted using AES/GCM/NoPadding. To decrypt:

1. The encrypted payload is sent in the request body
2. The initialization vector is provided in the `X-Teal-Encryption-IV` header (Base64 encoded)

Here's how to decrypt the payload in different languages:

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

  function decrypt(
      encryptedBase64,
      ivBase64,
      encryptionKeyBase64
  ) {
      const encryptionKey = Buffer.from(encryptionKeyBase64, 'base64');
      const iv = Buffer.from(ivBase64, 'base64');
      const encryptedBuffer = Buffer.from(encryptedBase64, 'base64');

      // Extract the auth tag (last 16 bytes) and ciphertext
      const authTag = encryptedBuffer.subarray(encryptedBuffer.length - 16);
      const ciphertext = encryptedBuffer.subarray(0, encryptedBuffer.length - 16);

      const decipher = crypto.createDecipheriv('aes-256-gcm', encryptionKey, iv);
      decipher.setAuthTag(authTag);
      let decrypted = decipher.update(ciphertext);
      try {
          const final = decipher.final();
          if (final.length > 0) {
              decrypted = Buffer.concat([decrypted, final]);
          }

          const payload = JSON.parse(decrypted.toString('utf8'));
          console.log('Decrypted payload:', payload);
      } catch (error) {
          console.error('Decryption failed:', error);
      }
  }

  decrypt(
      webhookRequest.body,
      webhookRequest.headers['x-teal-encryption-iv'],
      yourSecretStorage.base64EncryptionKey
  )
  ```

  ```python Python theme={null}
  import base64
  import json
  from Crypto.Cipher import AES

  def decrypt(
      encrypted_base64: str,
      iv_base64: str,
      encryption_key_base64: str
  ) -> dict:
      encryption_key = base64.b64decode(encryption_key_base64)
      print(f"Key length: {len(encryption_key)} bytes")
      iv = base64.b64decode(iv_base64)
      print(f"IV length: {len(iv)} bytes")

      encrypted_data = base64.b64decode(encrypted_base64)
      print(f"Encrypted data length: {len(encrypted_data)} bytes")

      auth_tag = encrypted_data[-16:]
      ciphertext = encrypted_data[:-16]
      print(f"Auth tag length: {len(auth_tag)} bytes")
      print(f"Ciphertext length: {len(ciphertext)} bytes")

      cipher = AES.new(encryption_key, AES.MODE_GCM, nonce=iv)

      decrypted_data = cipher.decrypt_and_verify(ciphertext, auth_tag)

      payload = json.loads(decrypted_data.decode('utf-8'))
      print("Decryption successful!")
      print(f"Decrypted payload: {payload}")
      return payload

  if __name__ == "__main__":
      decrypt(
          webhookRequest.body,
          webhookRequest.headers["x-teal-encryption-iv"],
          secretStorage.base64Secret
      )
  ```

  ```java Java theme={null}
  import javax.crypto.Cipher;
  import javax.crypto.spec.GCMParameterSpec;
  import javax.crypto.spec.SecretKeySpec;
  import java.util.Base64;

  public class DecryptMessage {

      public static String decryptWebhookPayload(
          String encryptedData,
          String encryptionKey,
          String ivBase64
      ) throws Exception {
          byte[] key = Base64.getDecoder().decode(encryptionKey);
          byte[] iv = Base64.getDecoder().decode(ivBase64);
          byte[] encryptedBytes = Base64.getDecoder().decode(encryptedData);

          SecretKeySpec secretKey = new SecretKeySpec(key, "AES");
          Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
          GCMParameterSpec spec = new GCMParameterSpec(128, iv);

          cipher.init(Cipher.DECRYPT_MODE, secretKey, spec);
          byte[] decryptedBytes = cipher.doFinal(encryptedBytes);

          return new String(decryptedBytes, "UTF-8");
      }

      public static void main(String[] args) {
          try {
              String result = decryptWebhookPayload(
                  "nBPqEB4T1g/88q1P2qm7oYH5MYbR/NpCVBCytMZsB7ux4xR+OYPS8Y/Ye8giQb6UplDitbKlFlHPoA6pzMu3CmD2cew4JvluyKiOQBE61OwETwJ++KkvwuB9WxegKnhncWqg6qwn9WkRGtmKpfrxiXrdvj7V4aKSOFkO8ByWYztQp58Ublm3yNPQyQmnzJOR+/4v8OMbVivcAkumRu8PO2EYr2uqfPvNMonXiNE5fnqJieZKFvRnDi+s6TNZpSsCzWPgfDGn+EqiDbjqWw+eWnUYd3BQ75OOJficovrzk1dYkONm5uYTnZYSdiwMzNm7",
                  "7/PxZATSzWbQkS8ZjSt0f+bTMt9oQ6jkm0aYFe3NN24=",
                  "vdBbe9Tk7aqo1pZi"
              );
              System.out.println("Decrypted payload: " + result);
          } catch (Exception e) {
              System.err.println("Decryption failed: " + e.getMessage());
              e.printStackTrace();
          }
      }
  }
  ```

  ```kotlin Kotlin theme={null}
  import java.util.Base64
  import javax.crypto.Cipher
  import javax.crypto.spec.GCMParameterSpec
  import javax.crypto.spec.SecretKeySpec

  fun decryptWebhookPayload(
      encryptedData: String,
      encryptionKey: String,
      ivBase64: String
  ): String {
      val key = Base64.getDecoder().decode(encryptionKey)
      val iv = Base64.getDecoder().decode(ivBase64)
      val encryptedBytes = Base64.getDecoder().decode(encryptedData)
      val secretKey = SecretKeySpec(key, "AES")
      val cipher = Cipher.getInstance("AES/GCM/NoPadding")
      val spec = GCMParameterSpec(128, iv)
      cipher.init(Cipher.DECRYPT_MODE, secretKey, spec)
      val decryptedBytes = cipher.doFinal(encryptedBytes)
      return String(decryptedBytes, Charsets.UTF_8)
  }

  fun main() {
      try {
          val result = decryptWebhookPayload(
              "nBPqEB4T1g/88q1P2qm7oYH5MYbR/NpCVBCytMZsB7ux4xR+OYPS8Y/Ye8giQb6UplDitbKlFlHPoA6pzMu3CmD2cew4JvluyKiOQBE61OwETwJ++KkvwuB9WxegKnhncWqg6qwn9WkRGtmKpfrxiXrdvj7V4aKSOFkO8ByWYztQp58Ublm3yNPQyQmnzJOR+/4v8OMbVivcAkumRu8PO2EYr2uqfPvNMonXiNE5fnqJieZKFvRnDi+s6TNZpSsCzWPgfDGn+EqiDbjqWw+eWnUYd3BQ75OOJficovrzk1dYkONm5uYTnZYSdiwMzNm7",
              "7/PxZATSzWbQkS8ZjSt0f+bTMt9oQ6jkm0aYFe3NN24=",
              "vdBbe9Tk7aqo1pZi"
          )
          println("Decrypted payload: $result")
      } catch (e: Exception) {
          println("Decryption failed: ${e.message}")
          e.printStackTrace()
      }
  }
  ```
</CodeGroup>

## Webhook Management

### Viewing subscriptions

List your current webhooks with a simple GET request to our webhook endpoint `GET /webhooks`.

### Deleting subscriptions

Unsubscribe using the webhook ID in the request `DELETE /webhooks/{webhook_id}`

## Best Practices

* **Secure Endpoints**: Ensure your webhook URL is HTTPS-secured to safeguard transmitted data.
* **Manage Secrets**: Keep your `signing_secret` and `encryption_key` confidential.
* **Use Encryption**: Consider using the encryption feature when your team doesn't control HTTPS termination and you want payloads to be decrypted only by applications possessing the encryption key. As an example, this could help prevent sensitive or GDPR-regulated information from being logged in systems not designed to handle such data (there might be more cases for your specific situation).
* **Alternative to Encryption**: As an alternative to encryption, consider disabling the payload in webhooks by not setting `include_payload` in your webhook configuration.
* **Regular Review**: Periodically check your webhook subscriptions and adjust as needed to match your application's requirements.

Leverage our webhooks to make your application more dynamic and responsive to changes without the overhead of continuous polling.

## Webhook Example

Example of webhook request body configured to include the payload by setting
`"include_payload": "true"` in POST ` /webhooks` request body.

If the payroll data comes from uploading a payslip the `account_id`
whould be `null` but `document_external_id` and `document_filename` would be populated

is related to uploading a payslip.

```json theme={null}
{
  "event": "user-payroll-submitted",
  "user_id": "e9b7676b-f107-429c-8d17-cf3fd1362fd3",
  "timestamp": "2024-04-17T13:56:03.277Z",
  "entry_id": "cc48a12c-0e98-47aa-b281-f203403470d0",
  "account_id": "f68961c9-a083-4bfd-a3c6-0ae169b48f35",
  "payload": {
    "entry_id": "cc48a12c-0e98-47aa-b281-f203403470d0",
    "account_id": "f68961c9-a083-4bfd-a3c6-0ae169b48f35",
    "payroll_submissions": [
      {
        "id": "0187c66e-e7e5-811c-b006-2232f00f426a",
        "account_id": "f68961c9-a083-4bfd-a3c6-0ae169b48f35",
        "entry_id": "cc48a12c-0e98-47aa-b281-f203403470d0",
        "created_at": "2024-04-17T13:56:03.118Z",
        "document_external_id": null,
        "document_filename": null,
        "source": "Payroll (via Direct Connection)",
        "identity_information": {
          "name": "Jane Doe",
          "date_of_birth": "1990-04-01",
          "address": {
            "street": "123 Main St",
            "county": "Anyshire",
            "city": "Anycity",
            "post_code": "AB12 3CD",
            "country": "Countryland"
          },
          "email": "jane.doe@example.com",
          "phone": "+1234567890",
          "NI_number": "AB123456C",
          "employment_information": {
            "employer_name": "Acme Corp",
            "role": "Software Developer",
            "type": "Full-Time",
            "status": "Active",
            "start_date": "2018-06-01",
            "leave_date": null
          },
          "income_information": {
            "pay_date": "2024-02-15",
            "pay_interval_start": "2024-02-01T00:00:00.000Z",
            "pay_interval_end": "2024-02-15T00:00:00.000Z",
            "pay_frequency": "weekly",
            "earnings": {
              "gross_pay": 2000,
              "net_pay": 1500,
              "base_salary": 1900,
              "bonus": 100
            },
            "deductions": {
              "income_tax": 300,
              "employee_ni": 150,
              "employee_pension": 50,
              "total_deductions": 500
            }
          }
        }
      }
    ]
  }
}
```

### Bank Statement Webhook Example

Example of a `user-bank-statement-submitted` webhook request body configured with `include_payload` enabled.

The `document_external_ids` array contains the external IDs provided during upload, if any.
When `include_payload` is enabled, the `payload` object contains the full bank statement
submission data including extracted account information, balances, and transactions.

```json theme={null}
{
  "event": "user-bank-statement-submitted",
  "user_id": "e9b7676b-f107-429c-8d17-cf3fd1362fd3",
  "timestamp": "2024-04-17T13:56:03.277Z",
  "document_external_ids": ["ext-stmt-2024-01"],
  "payload": {
    "bank_statement_submissions": [
      {
        "id": "95a0e70b-fe02-4f47-aef9-2efff279df71",
        "created_at": "2024-04-17T13:56:03.118Z",
        "document_id": "a9249254-ab10-4a2d-b709-eda95f5ecd59",
        "document_external_id": "ext-stmt-2024-01",
        "document_filename": "hsbc-january-2024.pdf",
        "account_information": {
          "account_holder_name": "JANE DOE",
          "account_number": "12345678",
          "sort_code": "12-34-56",
          "bank_name": "HSBC",
          "iban": null
        },
        "statement_period": {
          "start_date": "2024-01-01",
          "end_date": "2024-01-31"
        },
        "balance_information": {
          "opening_balance": "1,000.00",
          "closing_balance": "1,250.00"
        },
        "transactions": [
          {
            "date": "2024-01-15",
            "description": "Salary Payment",
            "amount": "2,500.00",
            "type": "credit"
          },
          {
            "date": "2024-01-20",
            "description": "Rent",
            "amount": "-1,250.00",
            "type": "debit"
          }
        ],
        "bank_statement_probability": 0.95
      }
    ]
  }
}
```
