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

# Callbacks & signature verification

> Coming in the next phase

<Warning>
  Callbacks aren't live yet. This page documents the design so you can plan
  for it; nothing below is callable today. Until it ships, poll
  [`GET /v1/auth/test`](/reference/overview) periodically to detect a
  disconnect (`401 token_revoked`) instead of waiting for `app.uninstalled`.
</Warning>

When callbacks ship, Workchats will POST signed events to your App's
registered callback URL — one URL per App, shared across every company that
installs it.

## Envelope

```json theme={null}
{
  "id": "evt_01J9ZK3…",
  "type": "directory.changed",
  "workspace_id": "cmp_…",
  "created_at": "2026-09-28T11:06:40Z",
  "data": { "kind": "user.deactivated", "id": "usr_9a1…" }
}
```

`id` is unique per event, so a retried delivery is safe to ignore if you've
already processed that id.

## Event types

| Type | Fires when | `data` |
| - | - | - |
| `app.uninstalled` | A company admin uninstalls your App, or Octogle disables it. Not sent for `POST /oauth/revoke` — that's a disconnect from your side, not an uninstall. | `{"removed_by": {"id": "usr_…"} \| null, "reason": "admin" \| "app_disabled"}` |
| `directory.changed` | A user or conversation your bot can see changes: `user.created`, `user.updated`, `user.deactivated`, `conversation.created`, `conversation.renamed`, `conversation.archived`, `conversation.deleted`, `app.removed_from_conversation` | `{"kind": "…", "id": "…"}` — re-read the user or conversation to get the new state |
| `interaction` | Someone clicks an action button on a message your bot sent (needs Phase 2's clickable buttons — see [Blocks](/guides/blocks)) | `action_id`, `value`, `user {id, email?}`, `message {id, conversation_id, metadata}` |

## Signature verification

Every callback carries:

```
X-Workchats-Timestamp: 1790593600
X-Workchats-Signature: v1=<hex>
```

`<hex>` is `HMAC-SHA256(signing_secret, "<timestamp>.<raw request body>")`,
hex-encoded. Verify against the **raw** request body — parsing to JSON and
re-serializing before checking the signature will not match.

```javascript Node theme={null}
import { createHmac, timingSafeEqual } from "node:crypto";

function isValidWorkchatsSignature(rawBody, timestamp, signatureHeader, signingSecret) {
  const expected = createHmac("sha256", signingSecret)
    .update(`${timestamp}.${rawBody}`)
    .digest("hex");

  return signatureHeader
    .split(",")
    .some((part) => {
      const hex = part.trim().replace(/^v1=/, "");
      return (
        hex.length === expected.length &&
        timingSafeEqual(Buffer.from(hex, "hex"), Buffer.from(expected, "hex"))
      );
    });
}
```

Reject anything where the signature doesn't match, or where the timestamp
is more than 5 minutes old. During a signing-secret rotation, Workchats
sends two comma-separated values — `v1=<new>,v1=<old>` — for 24 hours, so
check every value in the header rather than only the first.

## Delivery and retries

HTTPS only, TLS 1.2+, no redirects followed. A response other than `2xx`
within 3 seconds counts as a failure and is retried at 1 minute, 5 minutes,
and 30 minutes, with the same event `id`.

Respond `2xx` as soon as you've durably queued the event — don't do slow
work in the request path before responding.
