Team & admin
Integrations
Post to Slack, send signed webhooks to your services, and add the comity/coordination check to pull requests with the GitHub App.
On this page
Integrations are set up by owners and admins in the dashboard under Integrations. Other roles see them read-only. Linear, Jira and Discord are listed as "Coming soon"; they don't exist yet.
Slack
Comity posts to one Slack channel through a Slack incoming webhook. There is no Slack app to install, no OAuth and no slash commands.
Connect
- In Slack, create an incoming webhook and pick the channel.
- In Comity, open Integrations → Slack → Connect, paste the URL (
https://hooks.slack.com/services/T…/B…/…) and click Connect. - Click Send test message. The channel gets "Comity is connected".
Comity stores the URL encrypted and never shows it again. Replace URL swaps it; Disconnect removes it.
API equivalent (owner/admin personal token):
curl -X PUT https://api.comity.so/v1/integrations/slack \
-H "Authorization: Bearer $COMITY_TOKEN" -H "Content-Type: application/json" \
-d '{"webhook_url": "https://hooks.slack.com/services/T000/B000/XXXX"}'{"webhook_url": null} disconnects. GET /v1/integrations/slack shows connected, url_host, failing_since and the last_post (status pending, sending, sent, failed or superseded). POST /v1/integrations/slack/test sends the test message.
What gets posted
Controlled by the notify policy (in the Slack drawer under What gets posted, or Settings → Coordination policy → Slack notifications):
- New or escalated conflicts at or above the minimum severity. By default only blocking conflicts (
conflict.detected:block). - Agent action blocked (
hook.blocked), titled "Agent blocked in<repo>". - Resolved, overridden, went stale (
conflict.resolved,conflict.overridden,conflict.stale): posted when selected, and always for a conflict the channel was already told about, to close the loop.
Each post shows the repo, severity, subject, the two sides and the suggested next step. Updates to the same conflict are batched: at most one post per conflict per minute, later updates fold into it ("N more updates collapsed"). Failed posts (429, 5xx, network errors) retry with backoff for 24 hours.
Webhooks
Webhooks send signed JSON events to your own services: alerting, dashboards, a SIEM, automations. An org can have up to 20 endpoints.
Create an endpoint
Integrations → Webhooks → Add endpoint: enter an HTTPS URL, an optional description, and either All events or Choose events. After Create endpoint, copy the signing secret (whsec_…): it is shown only once. To rotate it, create a new endpoint and delete the old one.
curl -X POST https://api.comity.so/v1/webhooks \
-H "Authorization: Bearer $COMITY_TOKEN" -H "Content-Type: application/json" \
-d '{"url": "https://example.com/comity/events", "event_types": ["conflict.detected", "conflict.resolved", "hook.blocked"], "description": "on-call alerts"}'{
"id": "0199c3d1-…",
"url_host": "example.com",
"event_types": ["conflict.detected", "conflict.resolved", "hook.blocked"],
"description": "on-call alerts",
"active": true,
"disabled_reason": null,
"failing_since": null,
"last_success_at": null,
"created_at": "2026-10-19T10:02:11.000Z",
"secret": "whsec_…"
}event_types takes up to 64 event types, or ["*"] (the default) for every type except the noisy conflict.updated, including types added later. Other routes: GET /v1/webhooks, DELETE /v1/webhooks/:id, POST /v1/webhooks/:id/test, GET /v1/webhooks/:id/deliveries.
URLs must be HTTPS and resolve to a public address; private, loopback and internal addresses are rejected. Redirects are not followed.
Event types
| Group | Types |
|---|---|
| Conflicts | conflict.detected, conflict.updated, conflict.acknowledged, conflict.resolved, conflict.overridden, conflict.reopened, conflict.stale |
| Hooks | hook.blocked, hook.warned |
| Intents & claims | intent.declared, intent.updated, intent.status_changed, intent.closed, claim.added, claim.released, claim.expired, snapshot.recorded |
| Sessions | session.started, session.ended, session.expired, session.status_shared |
| Messages | message.sent, message.acked, question.asked, question.answered |
| Members | member.added, member.removed, member.role_changed, invite.created, invite.accepted, invite.revoked, token.created, token.revoked |
| Repos & projects | repo.created, project.created, project.updated, project.deleted |
| Org | org.created, org.data_mode_changed, policy.updated, integration.connected, integration.removed, integration.connect_rejected, billing.updated, billing.payment_failed |
Payload
Every delivery is a POST of one event envelope. Event payloads carry identifiers, counts and enums, never message text, code or secrets.
{
"id": "18342",
"type": "conflict.detected",
"version": 1,
"occurred_at": "2026-10-19T12:00:00.000Z",
"org_id": "0199a0b2-…",
"actor": { "kind": "session", "id": "0199c3a4-…" },
"subject": { "kind": "conflict", "id": "0199c3e0-…" },
"payload": { "repo_id": "0199a0c7-…", "number": 37, "kind": "contract_break", "severity": "warn", "confidence": 0.9, "a": "…", "b": "…" }
}actor.kind is user, session, system or token. A test delivery has type webhook.test and an empty payload.
Headers
| Header | Value |
|---|---|
X-Comity-Signature |
t=<unix seconds>,v1=<hex HMAC-SHA256> |
X-Comity-Event |
The event type |
X-Comity-Delivery |
Delivery id, the same across retries; use it to deduplicate |
Content-Type |
application/json |
User-Agent |
Comity-Webhooks/1 |
Verify the signature
The signature is a lowercase hex HMAC-SHA256 of "<t>.<raw request body>", keyed with the full whsec_… secret. Verify against the raw bytes before parsing JSON, compare in constant time, and reject timestamps older than 5 minutes.
import { createHmac, timingSafeEqual } from "node:crypto";
export function verifyComity(rawBody: string, header: string | undefined, secret: string, toleranceSec = 300): boolean {
if (!header) return false;
const parts = header.split(",").map((p) => p.trim());
const t = Number(parts.find((p) => p.startsWith("t="))?.slice(2));
if (!Number.isFinite(t) || Math.abs(Date.now() / 1000 - t) > toleranceSec) return false;
const expected = Buffer.from(createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex"));
return parts
.filter((p) => p.startsWith("v1="))
.some((p) => {
const got = Buffer.from(p.slice(3));
return got.length === expected.length && timingSafeEqual(got, expected);
});
}// Express: keep the raw body for verification
app.post("/comity/events", express.raw({ type: "application/json" }), (req, res) => {
const raw = req.body.toString("utf8");
if (!verifyComity(raw, req.header("X-Comity-Signature"), process.env.COMITY_WEBHOOK_SECRET!)) return res.sendStatus(401);
const event = JSON.parse(raw);
// handle event.type …
res.sendStatus(204);
});Delivery and retries
- Any 2xx response is success. Respond within a few seconds: the connection timeout is 5 s and the total timeout 10 s.
- Failures retry with exponential backoff (30 s, doubling, capped at 2 h, with jitter) for 24 hours, then the delivery is marked
failed. - An endpoint that keeps failing for 3 days is disabled (
disabled_reason: "deliveries failed for 3 days"). Send test re-enables it. - The endpoint drawer lists recent deliveries with status, HTTP status, attempts, last error and the payload.
GitHub App
The GitHub App lets Comity follow pushes and pull requests, including branches no agent hook reported, and publishes a comity/coordination check run on pull requests. Each org creates its own private app, owned by your GitHub account or organization.
Set up
- Integrations → GitHub App → Set up. Optionally enter a GitHub organization to own the app; empty creates it under your personal account.
- Create GitHub App sends you to GitHub with a prefilled app manifest. Confirm it.
- Back in Comity, Install on GitHub and pick the repositories.
The app (named Comity <org slug>) requests the minimum it needs:
| Permission | Access |
|---|---|
| Contents | read |
| Pull requests | write |
| Checks | write |
| Metadata | read |
Events: push, pull_request, create, delete. Installation is confirmed against the installing user's own GitHub access, and an installation can belong to only one Comity org.
What it does
- Pushes to non-default branches and pull requests (opened, reopened, synchronized, ready for review, base changed, closed) are compared with the base. With
data_modefull, Comity reads the changed files to analyze symbols and contracts in memory; withmetadata_onlyit records only paths and line counts. See Security & data. - Each branch becomes a participant in conflict detection: a pushed branch that no agent session is working on still shows up in teammates' conflicts.
- Check run
comity/coordinationon pull requests:
| Situation | Conclusion | Title |
|---|---|---|
A live conflict with severity block |
failure |
"N open conflicts, M blocking" |
| Other live conflicts | neutral |
"N open conflicts" |
| No live conflicts | neutral |
"No open coordination conflicts" |
The check never reports success. It is updated when the PR's conflicts change and when you re-run it on GitHub. To gate merges on it, add comity/coordination as a required status check in your branch protection.
Branches idle for 14 days are closed. Repositories beyond your plan's repo limit are skipped.