Help centerIntegrations

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

EventOn screenSent whencontent holds
add_postpost is createdSomeone posts on a case's Feednew: the post's text
modify_postpost is modifiedA post's text is changednew: the new text
delete_postpost is deletedA post is deletedold: its text
add_post_commentpost comment is createdSomeone comments on a postnew: the comment's text
modify_post_commentpost comment is modifiedA comment's text is changednew: the new text
delete_post_commentpost comment is deletedA comment is deletedold: its text
add_post_mediapost media is createdA file is added to a postnew: a link to the file
delete_post_mediapost media is deletedA file is removed from a postold: a link to the removed file
add_post_comment_mediapost comment media is createdA file is added to a commentnew: a link to the file
delete_post_comment_mediapost comment media is deletedA file is removed from a commentold: 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_post for 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 one add_post_media for 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 when to 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}
}
FieldWhat it holds
what.trigger.nameThe event, from the table above
what.content.new or what.content.oldThe text, or a link to the file
who.user_idWho wrote the post or comment, even when someone else changed or deleted it
who.post.post_idThe post, on post events
who.comment.comment_idThe comment, on comment events
who.comment.post_idThe post the comment is on
who.case.case_idThe case, by the ID the API uses
who.case.case_ref_idThe 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_idThe author's role on the case, such as role::clients. Missing if they've left the case.
who.host.host_idYour hub
when.datetime.datetimeWhen it happened, in UTC

The other fields are for the hub's own use; ignore them.

Related articles

Set up a webhook for feed activitySend your own system a message whenever a feed post, comment or attachment is added, changed or deleted.Integrations · 10 minFix a webhook that stopped sendingWhy a webhook stops after two failed deliveries or when it's inactive, and how saving it again turns it back on.Integrations