Help Center/Automation & CI/CD/API & SDK

Webhook Events and Signature Verification

Build a TestCollab webhook receiver using the supported event catalog, signed JSON envelope, HMAC verification, event deduplication, and resource versions.

On this page

Use this reference when building a receiver for TestCollab project Webhooks. TestCollab sends JSON POST requests to the HTTPS endpoints configured by an Administrator. For the setup steps, see Set Up Project Webhooks.

Supported webhook events

There are 15 business event types. A test plan groups test cases for execution, a test plan run is one iteration of that plan, and an executed test case is a case's execution record within a run and configuration.

Record Event types
Test case test_case.created, test_case.updated, test_case.deleted
Suite suite.created, suite.updated, suite.deleted
Test plan test_plan.created, test_plan.updated, test_plan.deleted, test_plan.archived, test_plan.unarchived, test_plan.status_changed
Executed test case executed_test_case.status_changed, executed_test_case.assigned
Test plan run test_plan_run.created

webhook.test is a separate synthetic event sent by Send test. It does not require a catalog subscription and makes one delivery attempt.

executed_test_case.assigned is sent when an execution gets an assignee or a different assignee. A new execution that already has an assignee, for example in a new run or after test cases are added to a plan, also sends it. Removing an assignee sends no event. The event names assigned_to in changed_fields, and previous_data holds the earlier assignee, or null for a first assignment. Plan snapshots include assigned_to, and execution snapshots include the execution's assignee.

Open Settings > Webhooks > Events > Catalog and select View example to inspect an illustrative payload. For actual saved values, select View payload under Occurred events.

The signed request envelope

The HTTP body is a JSON object with these fields:

Field Meaning
payload The event serialized as a JSON string.
timestamp The signing time for this attempt, in Unix seconds.
secret_version The version of the project's signing secret used for this attempt.
signature A hexadecimal HMAC-SHA256 signature.

Parse the outer JSON to read these fields. Verify the signature using the unchanged payload string before parsing the inner event. Do not reformat or reserialize that string first: even a whitespace change produces a different signature.

Verify the signature

Calculate HMAC-SHA256 over String(timestamp) + "." + payload, using the shared secret as a UTF-8 string. Although a generated secret looks hexadecimal, do not hex-decode it. Compare the calculated digest with signature using a constant-time comparison.

The following Node.js helper checks the signature, a five-minute timestamp window, the expected secret version, and the project ID. Store the secret in your receiver's secret store or an environment variable. Set the version to the value shown in TestCollab.

const crypto = require("crypto");

function verifyWebhook(envelope, secret, expectedVersion, expectedProject, now = Date.now()) {
  if (!secret || !envelope || typeof envelope.payload !== "string" ||
      !Number.isSafeInteger(envelope.timestamp) ||
      envelope.secret_version !== expectedVersion ||
      typeof envelope.signature !== "string" ||
      !/^[a-f0-9]{64}$/i.test(envelope.signature)) {
    throw new Error("Invalid webhook envelope");
  }

  const age = Math.abs(Math.floor(now / 1000) - envelope.timestamp);
  if (age > 300) throw new Error("Webhook timestamp outside the acceptance window");

  const expected = crypto.createHmac("sha256", secret)
    .update(String(envelope.timestamp) + "." + envelope.payload, "utf8")
    .digest();
  const received = Buffer.from(envelope.signature, "hex");
  if (!crypto.timingSafeEqual(expected, received)) {
    throw new Error("Invalid webhook signature");
  }

  const event = JSON.parse(envelope.payload);
  if (!event || event.schema_version !== "1" ||
      event.project !== expectedProject ||
      typeof event.event_id !== "string" || !event.event_id) {
    throw new Error("Unexpected webhook event");
  }
  return event;
}

Call this helper with your parsed request body and trusted receiver configuration. Reject verification failures before making changes in the receiving system. This helper verifies the request; your handler must also deduplicate events, validate the event-specific fields it uses, and process only the event types it supports.

The five-minute window is a receiver policy in this example. Keep your receiver's clock synchronized and choose a window appropriate for your integration. Use the envelope's timestamp, rather than the event's occurred_at, for this check. A retry signs the original event again with a new timestamp and the current secret.

Inside a business event payload

This shortened example shows the inner event after verification and parsing:

{
  "schema_version": "1",
  "event_id": "00000000-0000-4000-8000-000000000001",
  "event_type": "suite.updated",
  "occurred_at": "2026-10-28T09:00:00.000Z",
  "project": 123,
  "resource_type": "suite",
  "resource_id": 456,
  "resource_version": 2,
  "correlation_id": "00000000-0000-4000-8000-000000000002",
  "changed_fields": ["title"],
  "cause": {
    "action": "update",
    "root_resource_type": "suite",
    "root_resource_id": 456
  },
  "data": {
    "suite": { "id": 456, "title": "Checkout regression" }
  }
}
  • event_id identifies the event across automatic retries and manual replays.
  • resource_type and resource_id identify the changed record.
  • resource_version increases for events belonging to that record in the project.
  • correlation_id groups related events from an operation. See Handle bulk changes.
  • changed_fields names the fields associated with the change.
  • cause describes the action and its originating record, including cascaded changes.
  • data contains the saved snapshot under the resource type's key. For deleted records it contains the preceding snapshot. Some events also include previous_data, for example a status transition or an assignment.

Test case and test plan snapshots include custom fields. Test plan status snapshots include a numeric status, its label, and progress information. Execution status snapshots use a status string and identify the test case, plan, run, configuration, and assignee. Use the catalog and actual event payloads when mapping these fields to your system; examples are not full API resource responses.

Handle duplicate and out-of-order events

Delivery is at least once, so your receiver must accept the possibility of repeat requests. Persist processed event_id values and make side effects idempotent. If a request timed out after you processed it, acknowledge the duplicate without repeating the operation. Coordinate deduplication with your own write or durable queue so a crash cannot silently lose the event.

Do not assume arrival order. When maintaining a current-state copy, compare resource_version for the same project, resource type, and resource ID before replacing a newer snapshot. If your workflow needs every transition, process each distinct event ID even when a later snapshot has already arrived.

Return an HTTP 2xx response after you have accepted the event durably. TestCollab treats any 2xx response as delivered, regardless of an error described in its body. Return an appropriate non-2xx response for a failure and inspect the retry behavior when designing your handler. General HTTP redirects are not followed, so configure the final receiver URL.

Handle bulk changes

One action in TestCollab can change many records at once. TestCollab then sends one event for each changed record, not one event for the action. All events of one action carry the same correlation_id.

These actions can send many events:

  • A bulk edit of test cases or test plans
  • An import of test cases, or a BDD feature file sync
  • A copy of test cases, suites or test plans
  • Move to Trash or Restore, for example a suite with all its test cases
  • A bulk status change of results in a test plan run
  • A rename of a custom field or its label: every test case or test plan that uses the field changes
  • The removal of a project member, when the member's default assignments move to another person

To handle a bulk change in your receiver:

  1. Group the events by correlation_id. cause.action tells you what the person did, for example import, copy, trash or status_change. cause.root_resource_type and cause.root_resource_id name the record that the person acted on. A record that changed because of another record has the action cascade.
  2. Do not expect the whole group at once. TestCollab delivers a large group in parts, so the last events can arrive several minutes after the first. Treat a group as complete when no new event with its correlation_id arrives for a few minutes. Then act once, for example with one chat message: "120 test cases imported".
  3. Accept each event quickly. Store it and return a 2xx response, then do the slow work later. A slow response makes the delivery time out, and TestCollab retries a delivery that timed out.
  4. Keep the rules for single events. Remove duplicates by event_id and order by resource_version, as in Handle duplicate and out-of-order events.
  5. Subscribe only to the events you need. An endpoint receives only the event types that you select for it, so a smaller selection means fewer events in a bulk change.

Last updated 2026-10-29.