For: Admins
Look up webhook events and payloads
What each of the ten feed events sends, how the message is encrypted, and how to decrypt it with your secret.
In this article4 sections
This page lists the events a webhookWebhook An address you give the hub so it can send your own systems a message whenever a feed post or comment changes. sends, how each message arrives, and what it holds once decrypted.
Events
| Event | On screen | Sent when | content holds |
|---|---|---|---|
add_post | post is created | Someone posts on a case's Feed | new: the post's text |
modify_post | post is modified | A post's text is changed | new: the new text |
delete_post | post is deleted | A post is deleted | old: its text |
add_post_comment | post comment is created | Someone comments on a post | new: the comment's text |
modify_post_comment | post comment is modified | A comment's text is changed | new: the new text |
delete_post_comment | post comment is deleted | A comment is deleted | old: its text |
add_post_media | post media is created | A file is added to a post | new: a link to the file |
delete_post_media | post media is deleted | A file is removed from a post | old: a link to the removed file |
add_post_comment_media | post comment media is created | A file is added to a comment | new: a link to the file |
delete_post_comment_media | post comment media is deleted | A file is removed from a comment | old: a link to the removed file |
- Every case in your hub sends them, whoever made the change: staff, a client, the Virtual AssistantVirtual Assistant An automated team member that posts welcome messages, check-ins and stage updates for the firm; its posts carry a Virtual Assistant badge., an import or an API key. A post's or comment's events still come after its author has left the case.
- Deleting a case sends nothing, not even
delete_postfor the posts its deletion removes. Once it's deleted, the API answers 404 for the case. - Only posts and comments on posts send events. Notes, tasks, forms, e-signs, events and their comments send nothing, and so does a change to who can see a post.
- A post with files sends
add_post, then oneadd_post_mediafor each file. - A file link works for 48 hours. A removed file is deleted soon after, so the link in a delete event may not open.
- A mention in the text reads
<@, the person's user ID and>, such as<@cu-4f1c2a9e8b7d60>.
Delivery
- Each message is a POST to the webhook's address, with a JSON body holding one field,
payload: the encrypted message. - Any status below 400 within 10 seconds counts as delivered.
- A delivery that fails isn't sent again. Two in a row stop the webhook; see Fix a webhook that stopped sending.
- A message can arrive twice, or out of order. Use the event, the item's ID and
whento tell.
{"payload": "eyJhbGciOiJSU0EtT0FFUCIsImVuYyI6IkExMjhDQkMtSFMyNTYifQ.…"}
Decrypting
payload is a JWE in compact form, encrypted with RSA-OAEP and A128CBC-HS256. The webhook's secret is the matching private key, a PEM file that starts -----BEGIN PRIVATE KEY-----. Use the downloaded file, or the secret copied whole with its line breaks. Any JOSE library can decrypt it.
Python, with jwcrypto:
import json
from jwcrypto import jwk, jwt
with open("webhook-private-key.pem", "rb") as f:
key = jwk.JWK.from_pem(f.read())
def read_message(body):
token = jwt.JWT(jwt=body["payload"], key=key, expected_type="JWE")
return json.loads(token.claims)
Node.js, with jose:
import { readFileSync } from 'node:fs';
import { compactDecrypt, importPKCS8 } from 'jose';
const key = await importPKCS8(readFileSync('webhook-private-key.pem', 'utf8'), 'RSA-OAEP');
export async function readMessage(body) {
const { plaintext } = await compactDecrypt(body.payload, key);
return JSON.parse(new TextDecoder().decode(plaintext));
}
There's no separate signature. Your hub never shows the key it encrypts with, so a message that decrypts with your secret came from your hub, and one changed on the way fails to decrypt. If several hubs send to the same address, check who.host.host_id as well.
The message
A decrypted add_post:
{
"who": {
"user_id": "cu-4f1c2a9e8b7d60",
"cc_owner": false,
"post": {"post_id": "post::20261001140322_9f8e7d6c5b4a32"},
"case": {"case_id": "cc-1a2b3c4d5e6f70", "case_ref_id": "rear-end-2026"},
"role": {"role_id": "role::staff"},
"host": {"host_id": "ca-7e6d5c4b3a2f10"}
},
"what": {
"trigger": {"name": "add_post"},
"content": {
"new": "Hi Jane, your records request went out today.",
"type": "app_add_post",
"kwargs": {"case_id": "cc-1a2b3c4d5e6f70", "case_ref_id": "rear-end-2026"}
}
},
"where": {"location": {"name": "app"}, "module": {"name": "post"}},
"when": {"datetime": {"timezone": "utc", "datetime": "2026-10-01T14:03:22.123456+0000"}},
"targets": {"user_ids": ["cu-4f1c2a9e8b7d60"], "cc_user_ids": [], "group_targets": false}
}
| Field | What it holds |
|---|---|
what.trigger.name | The event, from the table above |
what.content.new or what.content.old | The text, or a link to the file |
who.user_id | Who wrote the post or comment, even when someone else changed or deleted it |
who.post.post_id | The post, on post events |
who.comment.comment_id | The comment, on comment events |
who.comment.post_id | The post the comment is on |
who.case.case_id | The case, by the ID the API uses |
who.case.case_ref_id | The case's Tag IDTag ID A permanent label you give a case, such as rear-end-2026, also shown as Reference ID and used in imports. |
who.role.role_id | The author's role on the case, such as role::clients. Missing if they've left the case. |
who.host.host_id | Your hub |
when.datetime.datetime | When it happened, in UTC |
The other fields are for the hub's own use; ignore them.