Skip to main content

Using Webhooks to Keep External Applications Up-to-Date

Webhooks are a powerful way to receive real-time notifications from Coalesce Quality when specific events occur. This allows external applications to stay updated without constant polling, saving resources and ensuring timely updates.

Introduction to Webhooks

Coalesce Quality sends HTTP POST requests to a predefined URL whenever certain events occur, including issue lifecycle changes, incident notifications, or a simple ping to confirm that the webhook endpoint is reachable. Webhooks are defined by a schema that ensures the correct data structure is maintained. Each event has a specific payload format, allowing external applications to process the data accordingly. Latest schema definition is available as a JSON Schema and as HTML documentation.

Key Concepts

  • Event Types: Webhook events can represent different actions, such as ping, issue_created, issue_updated, issue_status_updated, issue_closed, incident_open, incident_closed, and incident_cancelled.
  • Payload Structure: Each event payload adheres to a defined JSON schema, ensuring consistency and reliability in the data received.
  • Callback Mechanisms: Webhooks can trigger specific commands or actions in the receiving application, making it possible to automate workflows based on incoming events.

Webhook Event Schema

The webhook event schema defines the structure of the payload sent for each event type:
  • workspace: Identifies the workspace where the event occurred.
  • event_id: A unique identifier for the event.
  • event_time: The time when the event occurred, formatted as a date-time string.
  • event_type: One of the specific event types (ping, issue_created, issue_updated, issue_status_updated, issue_closed, incident_open, incident_closed, incident_cancelled), each with its own structured payload.
  • callbacks: An array of callbacks that can be invoked based on the event, containing details such as url, action_name, and associated issues_command.

Event Types

Issue Events

  • Issue Created: Notifies that a new issue has been created. Contains an IssueSummary with details such as issue_id, title, description, status, trigger and affected entities.
  • Issue Updated: Indicates that an existing issue has been updated.
  • Issue Status Updated: Signals that the status of an issue has changed (e.g., investigating, expected, fixed, no action needed).
  • Issue Closed: Signals that an issue has been closed.

Incident Events

  • Incident Open: Notifies that a new incident has been opened. Contains an IncidentSummary with incident_id, title, description, and incident_url.
  • Incident Closed: Indicates that an incident has been closed.
  • Incident Cancelled: Signals that an incident has been cancelled.

Other Events

  • Ping: A simple test event sent during webhook setup to confirm the endpoint is functional. Payload contains a message.

Working with Webhook Events

To start using webhooks:
  1. Register a Webhook Endpoint: Provide a URL endpoint where Coalesce Quality can send events. Use Settings > Integrations > Add integration > Webhook.
  2. Handle Incoming Events: Set up your server to process incoming POST requests. Ensure that your endpoint correctly interprets the payload format defined in the webhook event schema. Return 2xx status codes to confirm receipt.
  3. Verify the Signature (recommended): Confirm each request genuinely came from Coalesce Quality before acting on it. See Verifying Webhook Signatures.
  4. Automate Actions: Use the event data to trigger specific actions in your application, such as updating a database, notifying users, or calling other APIs.

Custom request headers

You can attach custom HTTP headers to every webhook request under Settings > Integrations > your webhook integration — for example an Authorization header so Coalesce Quality can reach a protected endpoint. Mark a header secret to store its value securely instead of in the plaintext configuration: secret header values are write-only — masked in the UI and API responses and never returned — and are sent only in the outgoing request. Non-secret headers keep a visible, editable value. To change a secret header enter a new value; leave it blank to keep the stored one.
For authenticating that a request genuinely came from Coalesce Quality, prefer signature verification over a static token in a custom header.

Verifying Webhook Signatures

Every webhook delivery is signed so your endpoint can verify that the request genuinely originated from Coalesce Quality and was not tampered with or replayed. Verification is strongly recommended — it is the most robust way to authenticate incoming webhooks (stronger than a static bearer token in a custom header).

Signing secret

Each webhook integration has its own signing secret, generated by Coalesce Quality. The secret is shown once, at the moment it is generated — when you create the webhook integration and when you rotate it. Copy it then and store it somewhere secure; it is not retrievable afterwards. The value is prefixed with whsec_; use the entire string, including the prefix, as the HMAC key. If you lose the secret, rotate it (Settings > Integrations > your webhook integration) to obtain a fresh one. Rotating without downtime: when you rotate, the previous secret stays valid for a 24-hour grace window. During that window every delivery is signed with both the new and previous secrets (two values in X-Coalesce-Signature), so your endpoint keeps verifying whether it holds the old or the new secret. Update your endpoint to the new secret any time within the window.

Signature headers

Two headers are sent on every request:
HeaderDescription
X-Coalesce-TimestampUnix timestamp (seconds) of when the delivery was signed.
X-Coalesce-SignatureOne or more space-separated, scheme-prefixed signatures — e.g. v1=<hex> or, during a rotation grace window, v1=<hex-new> v1=<hex-old>. A request is authentic if any v1= value matches. The v1= prefix identifies the signature scheme.

Signature scheme

Each signature is a hex-encoded HMAC-SHA256 computed over the timestamp and the raw request body, joined by a .:
signed_payload = "{X-Coalesce-Timestamp}." + <raw request body>
signature      = hex( HMAC_SHA256(key = signing_secret, msg = signed_payload) )
To verify a request:
  1. Read the X-Coalesce-Timestamp header and the raw request body (verify before any JSON parsing or re-serialization — a re-serialized body will not match).
  2. Recompute the signature with your copy of the signing secret and check it against each v1= value in X-Coalesce-Signature using a constant-time comparison; accept the request if any matches.
  3. Optionally reject requests whose timestamp is outside your tolerance window. Retries reuse the original signing timestamp, so allow for the retry backoff window (deliveries may be retried for up to ~30 minutes).

Examples

import hmac
import hashlib

def verify(signing_secret: str, timestamp: str, raw_body: bytes, signature_header: str) -> bool:
    signed_payload = timestamp.encode() + b"." + raw_body
    expected = hmac.new(signing_secret.encode(), signed_payload, hashlib.sha256).hexdigest()
    # signature_header is one or more space-separated "v1=<hex>" values;
    # accept the request if any of them matches.
    for token in signature_header.split():
        scheme, _, provided = token.partition("=")
        if scheme == "v1" and hmac.compare_digest(expected, provided):
            return True
    return False

# In your handler (e.g. Flask): pass request.headers and request.get_data() (raw bytes)
const crypto = require("crypto");

function verify(signingSecret, timestamp, rawBody, signatureHeader) {
  const signedPayload = `${timestamp}.${rawBody}`;
  const expected = crypto
    .createHmac("sha256", signingSecret)
    .update(signedPayload)
    .digest("hex");
  // signatureHeader is one or more space-separated "v1=<hex>" values;
  // accept the request if any of them matches.
  return signatureHeader.split(" ").some((token) => {
    const [scheme, provided] = token.split("=");
    return (
      scheme === "v1" &&
      provided &&
      provided.length === expected.length &&
      crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(provided))
    );
  });
}

// rawBody must be the unparsed request body (e.g. express.raw()), not a re-stringified object.

Sample Webhook Event Payload

Here’s an example of a webhook event payload for an issue created:
{
  "workspace": "example_workspace",
  "event_id": "123456",
  "event_time": "2024-09-06T12:34:56Z",
  "event_type": "EVENT_TYPE_ISSUE_CREATED",
  "issue_created": {
    "issue": {
      "issue_id": "789",
      "issue_group_id": "group1",
      "issue_url": "https://app.synq.io/issues/789",
      "title": "Freshness check failed for orders table",
      "description": "Table has not been updated in the last 2 hours",
      "status": "ISSUE_STATUS_INVESTIGATING"
    }
  }
}