---
title: 'Webhooks Core Concepts: triggers, payloads and delivery'
description: 'Understand how TestingBot webhooks work: when they fire, default versus
  custom payloads, authorization options, delivery guarantees and limits.'
source_url:
  html: https://testingbot.com/support/integrations/webhooks/core-concepts
  md: https://testingbot.com/support/integrations/webhooks/core-concepts/index.md
---
# Webhooks Core Concepts

This page explains how TestingBot webhooks behave: when they fire, what they send, how the request is authorized and what delivery guarantees apply. If you have not created a webhook yet, start with the [Quick Start](https://testingbot.com/support/integrations/webhooks/quick-start).

## When webhooks fire

A webhook fires after each completed test that matches its trigger settings. You configure three filters when creating or editing a webhook at [/members/integrations/webhooks](https://testingbot.com/members/integrations/webhooks):

- **Result filter** : fire on **failures only** , or on all tests regardless of their result. 
- **Test type** : include all tests, only **automated browser tests** or only **real device tests**. 
- **Name filter** : an optional wildcard pattern such as `regression_*`. The webhook only fires when the pattern matches the test name or the build name. 

Requests may arrive with a short delay after a test completes.

## Default and custom payloads

Each webhook sends one of two payload types, selected with the **Default | Custom** toggle in the webhook form:

- **Default** : a fixed JSON document with all the test details, documented in the [reference below](https://testingbot.com#default-payload). This is the easiest option when your own endpoint consumes the request. 
- **Custom** : a JSON body that you write yourself, using [built-in variables](https://testingbot.com/support/integrations/webhooks/built-in-variables) such as `{{TEST_NAME}}` and `{{TEST_STATUS}}`. Use this to match the format an external service expects, or pick one of the [templates](https://testingbot.com/support/integrations/webhooks/integrations) to prefill a custom payload for a popular service. 

## Default payload reference

When you do not configure a custom payload, your webhook URL receives a POST request in JSON format with the following payload:

| Data Field | Format | Description |
| --- | --- | --- |
| `id` | STRING | The unique id of the test. Usually this will be a WebDriver SessionId, or Playwright/Puppeteer unique id. |
| `creation_time` | DATETIME | The date-time value, in `YYYY-MM-DDTHH:mm:ssZ` format, at which the test launched. |
| `completion_time` | DATETIME | The date-time value, in `YYYY-MM-DDTHH:mm:ssZ` format, at which the test was completed. |
| `user_id` | INTEGER | The id of the user who launched the test. |
| `team_id` | INTEGER | The id of the team for which this test was run. |
| `status` | ENUM | The status of the test, which can be one of the following values: 
- `PASSED`
- `FAILED`
- `UNKNOWN`

 |
| `name` | STRING | The name of the test, if it was passed through the [capabilities](https://testingbot.com/support/web-automate/selenium/test-options#name) or [set by API](https://testingbot.com/support/api#updatetest). |
| `browser_name` | STRING | The name of the browser on which this test ran. In case of a mobile app test, this might be `NULL`. |
| `device_name` | STRING | The name of the device on which this test ran. Might be `NULL` when this test ran on a Desktop environment. |
| `browser_version` | STRING | The version of the browser on which this test ran. In case of a mobile app test, this might be `NULL` or the device's version. |
| `platform` | STRING | The OS on which this test ran. |
| `duration_sec` | INTEGER | How long the test took to complete, the duration is in seconds. |
| `build` | STRING | The build this test belongs to, if it was passed through the [capabilities](https://testingbot.com/support/web-automate/selenium/test-options#build) or [set by API](https://testingbot.com/support/api#updatetest). |
| `tags` | ARRAY | Array of strings that were added for this test, if it was passed through the [capabilities](https://testingbot.com/support/web-automate/selenium/test-options) or [set by API](https://testingbot.com/support/api#updatetest). |
| `errors` | ARRAY | Array of error objects that were reported for this test. Empty if no errors were reported. The error objects are structured like this: 

    errors = [
      {
        message: "The error message",
        test_name: "The name of the sub test, if any. For example a Flow name when testing with Maestro"
      }
    ]

 |

## Authorization, headers and parameters

The **Authorization** , **Headers** and **Params** tabs in the webhook form control how the HTTP request is sent. The tabs appear when the **Custom** payload mode is selected; with the **Default** payload, only the Authorization fields are shown:

- **Authorization** : choose **No Auth** , **Basic Auth** (username and password) or a **Bearer token**. The credentials are added to the `Authorization` header of every request. 
- **Headers** : add custom HTTP headers, for example an API key header that the receiving service requires. 
- **Params** : add query parameters that are appended to the webhook URL. 

Header values, query parameter values and the URL itself support [built-in variables](https://testingbot.com/support/integrations/webhooks/built-in-variables), so you can include test details such as `{{TEST_ID}}` outside of the payload body.

## Delivery and security

- Delivery is **best effort** and is **not retried** : make sure your endpoint is available and responds quickly. 
- Your endpoint is expected to return a **2xx** status code. 
- Requests time out after **90 seconds**. 
- **Redirects are not followed** : point the webhook directly at the final URL. 
- Every delivery is **signed** with an HMAC-SHA256 signature, so your endpoint can [verify that the request really comes from TestingBot](https://testingbot.com#verify). You can additionally configure [authorization](https://testingbot.com#auth) as a second factor. 
- Webhook URLs that resolve to **private or internal addresses are refused** ; the endpoint must be reachable on the public internet. 

## Verify deliveries

Each webhook has a **signing secret** (it starts with `whsec_`), shown on the webhook's edit page in the [members area](https://testingbot.com/members/integrations/webhooks), where you can also rotate it. TestingBot signs every delivery with it and adds two headers to the request:

| Header | Contents |
| --- | --- |
| `X-TestingBot-Timestamp` | The Unix timestamp (seconds) at which the request was signed. |
| `X-TestingBot-Signature` | `sha256=<hexdigest>`: the HMAC-SHA256 of `"<timestamp>.<raw request body>"`, computed with your signing secret. |

To verify a delivery on your server:

1. Read the **raw request body** , before any JSON parsing or re-serialization. A single re-encoded space breaks the signature.
2. Reject the request when the timestamp is **older than 5 minutes** ; this blocks replayed deliveries.
3. Compute the HMAC-SHA256 of `"<timestamp>.<raw body>"` with your signing secret and prefix it with `sha256=`.
4. Compare it with the `X-TestingBot-Signature` header using a **constant-time comparison** , never a plain string equality.

[Java](https://testingbot.com#)[NodeJS](https://testingbot.com#)[Ruby](https://testingbot.com#)[Python](https://testingbot.com#)[C#](https://testingbot.com#)

    import javax.crypto.Mac;
    import javax.crypto.spec.SecretKeySpec;
    import java.nio.charset.StandardCharsets;
    import java.security.MessageDigest;
    
    public class TestingBotWebhook {
      public static boolean verifySignature(String rawBody, String timestamp,
                                            String signature, String secret) throws Exception {
        if (Long.parseLong(timestamp) < (System.currentTimeMillis() / 1000) - 300) {
          return false;
        }
    
        Mac mac = Mac.getInstance("HmacSHA256");
        mac.init(new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
        byte[] digest = mac.doFinal((timestamp + "." + rawBody).getBytes(StandardCharsets.UTF_8));
    
        StringBuilder hex = new StringBuilder();
        for (byte b : digest) {
          hex.append(String.format("%02x", b));
        }
        String expected = "sha256=" + hex;
    
        // MessageDigest.isEqual is a constant-time comparison.
        return MessageDigest.isEqual(
          expected.getBytes(StandardCharsets.UTF_8),
          (signature == null ? "" : signature).getBytes(StandardCharsets.UTF_8));
      }
    }

    const crypto = require('crypto');
    
    function verifyTestingBotSignature(rawBody, timestamp, signature, secret) {
      if (Number(timestamp) < Math.floor(Date.now() / 1000) - 300) return false;
    
      const expected = 'sha256=' + crypto.createHmac('sha256', secret)
        .update(`${timestamp}.${rawBody}`)
        .digest('hex');
      const a = Buffer.from(expected);
      const b = Buffer.from(String(signature || ''));
      return a.length === b.length && crypto.timingSafeEqual(a, b);
    }
    
    // Express needs the raw body, for example:
    // app.use(express.json({ verify: (req, res, buf) => { req.rawBody = buf.toString(); } }));

    require 'openssl'
    require 'rack/utils'
    
    def verify_testingbot_signature(raw_body, timestamp, signature, secret)
      return false if timestamp.to_i < Time.now.to_i - 300
    
      expected = 'sha256=' + OpenSSL::HMAC.hexdigest('SHA256', secret, "#{timestamp}.#{raw_body}")
      Rack::Utils.secure_compare(expected, signature.to_s)
    end
    
    # Rails example:
    # verify_testingbot_signature(
    # request.raw_post,
    # request.headers['X-TestingBot-Timestamp'],
    # request.headers['X-TestingBot-Signature'],
    # ENV['TESTINGBOT_WEBHOOK_SECRET']
    # )

    import hashlib
    import hmac
    import time
    
    def verify_testingbot_signature(raw_body, timestamp, signature, secret):
        if int(timestamp) < int(time.time()) - 300:
            return False
    
        signed = f"{timestamp}.{raw_body}".encode()
        expected = "sha256=" + hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest()
        return hmac.compare_digest(expected, signature or "")

    using System;
    using System.Security.Cryptography;
    using System.Text;
    
    public static class TestingBotWebhook
    {
        public static bool VerifySignature(string rawBody, string timestamp,
                                           string signature, string secret)
        {
            if (long.Parse(timestamp) < DateTimeOffset.UtcNow.ToUnixTimeSeconds() - 300)
            {
                return false;
            }
    
            using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(secret));
            var digest = hmac.ComputeHash(Encoding.UTF8.GetBytes($"{timestamp}.{rawBody}"));
            var expected = "sha256=" + Convert.ToHexString(digest).ToLowerInvariant();
    
            // FixedTimeEquals is a constant-time comparison.
            return CryptographicOperations.FixedTimeEquals(
                Encoding.UTF8.GetBytes(expected),
                Encoding.UTF8.GetBytes(signature ?? string.Empty));
        }
    }
    
    // ASP.NET Core needs the raw body, for example:
    // using var reader = new StreamReader(Request.Body);
    // var rawBody = await reader.ReadToEndAsync();

The **Test Webhook** button signs its request with the webhook's real secret when you test a saved webhook, so you can exercise your verification code from the edit page before any real test fires. After rotating the secret, update your receiver immediately: deliveries signed with the old secret no longer validate.

## Limits

- You can create up to **5 webhooks per team**. 
- A custom payload template can be at most **64,000 characters**. 
- The response shown in the **Test Webhook** panel is truncated to **10,000 characters**. 

## Related

- [Built-in variables](https://testingbot.com/support/integrations/webhooks/built-in-variables): every variable you can use in custom payloads, URLs, headers and parameters.
- [Integrations](https://testingbot.com/support/integrations/webhooks/integrations): ready-made templates for Slack, Teams, Discord, PagerDuty, OpsGenie and Jira.
- [Quick Start](https://testingbot.com/support/integrations/webhooks/quick-start): create and test your first webhook step by step.

### Looking for more help?

Have questions or need more information? Reach out via email or Slack.

[Email us](https://testingbot.com/contact/new)[Slack Join our Slack](https://join.slack.com/t/testingb0t/shared_invite/zt-3bcw9xch-jk19~6XPs_xBrsAgAedkCw)
