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

# Using Webhooks

> Getting started with developer API at Coalesce Quality

# 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](https://schemas.synq.io/webhook/v1/event.schema.json) and as [HTML documentation](https://schemas.synq.io/webhook/v1/event.html).

### 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](#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.

<Note>
  For authenticating that a request genuinely came from Coalesce Quality, prefer [signature verification](#verifying-webhook-signatures) over a static token in a custom header.
</Note>

## 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:

| Header                 | Description                                                                                                                                                                                                                                         |
| ---------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `X-Coalesce-Timestamp` | Unix timestamp (seconds) of when the delivery was signed.                                                                                                                                                                                           |
| `X-Coalesce-Signature` | One 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

<CodeGroup>
  ```python Python theme={null}
  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)
  ```

  ```javascript Node.js theme={null}
  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.
  ```
</CodeGroup>

## Sample Webhook Event Payload

Here's an example of a webhook event payload for an issue created:

```json theme={null}
{
  "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"
    }
  }
}
```
