Skip to main content

How to receive events with webhooks

Instead of asking every few minutes whether a meeting is done, let Sally tell you. A webhook sends a POST request to your URL as soon as a summary is ready. That is the difference between an integration that feels instant and one that feels like a nightly job.

This guide registers a webhook, builds a small receiving endpoint in Node and Python, and shows how to pause, change and remove webhooks. Every example works as it stands once SALLY_TOKEN and DIRECTORY_ID are set, see the Quickstart. It helps to know the summary shape from Get meeting summaries.

Quick navigation

  1. Register a webhook
  2. The events
  3. Personal or company-wide
  4. Receive the delivery
  5. On your side
  6. Pause, change or remove a webhook
  7. Complete setup script
  8. Endpoints at a glance

1. Register a webhook​

POST/v1.0/directories/{directoryId}/webhooks registers a webhook. The body takes these fields:

FieldTypeDefaultDescription
urlstringRequired. The endpoint Sally sends the POST request to.
eventstringsummary.readyWhen the webhook fires: summary.ready or manual, see The events.
namestringA label to recognize the webhook in the list.
directoryWidebooleanfalsefalse fires only for your own recordings, true for the recordings of all users. See Personal or company-wide.
curl -X POST "https://api.sally.io/v1.0/directories/$DIRECTORY_ID/webhooks" \
-H "Authorization: Bearer $SALLY_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"url": "https://example.com/hooks/sally",
"event": "summary.ready",
"name": "CRM sync"
}'

The answer is the new webhook:

200 OK
{
"webhookId": "af39409b-ca90-443d-a5e8-47dfd97c518f",
"name": "CRM sync",
"event": "summary.ready",
"url": "https://example.com/hooks/sally",
"isActive": true,
"directoryWide": false,
"createdOn": "2026-09-24T09:30:00Z"
}
FieldTypeMeaning
webhookIdstringId of the webhook. Keep it to pause, change or remove the webhook later.
namestringDisplay name of the webhook.
eventstringWhen the webhook fires.
urlstringTarget URL that Sally sends the event to.
isActivebooleanWhether the webhook is actively firing.
directoryWidebooleantrue for all users of the company account, false for your own recordings only.
createdOnstringCreation timestamp (ISO 8601).

2. The events​

  • summary.ready fires after every summary. This is the default and usually the one you want.
  • manual never fires on its own. A person starts it from a meeting or a recording, which is useful when someone should decide what gets sent.

Webhooks created before the current model may still report the older events recording.summarized or meeting.summarized. They stay readable but can no longer be set.

3. Personal or company-wide​

By default a webhook is personal and fires only for your own recordings. With "directoryWide": true it fires for the recordings of all users of the company account. Only admins and owners can create company-wide webhooks.

The same split applies when you read and manage webhooks:

RoleGET/v1.0/directories/{directoryId}/webhooks showsMay change, pause and remove
Admin or ownerAll webhooks of the company account, personal and company-wideAny webhook of the company account
Regular userOnly their own personal webhooksOnly their own personal webhooks

4. Receive the delivery​

When the event fires, Sally sends a POST request with a JSON body to your URL. The payload contains the summary with its section items, in the same shape as in Get meeting summaries, so one parser serves both paths. These are the fields the example handlers use:

FieldTypeMeaning
recordingSummaryIdstringId of the summary. Use it as the key for idempotent handling.
recordingIdstringThe recording the summary belongs to.
appointmentIdstring or nullThe appointment, if there is one.
summarystring or nullSummary text.
sectionItemsarrayThe structured results per section of the meeting template. Items with sectionType tasks carry subject, responsibleUserName and dueDate.

A minimal endpoint answers right away and does the work afterwards. The cURL tab shows how to test your endpoint locally with a real summary before Sally sends the first one.

# Fetch a real summary and send it to your local endpoint, like Sally would.
curl -s "https://api.sally.io/v1.0/directories/$DIRECTORY_ID/summaries/$RECORDING_SUMMARY_ID" \
-H "Authorization: Bearer $SALLY_TOKEN" > summary.json

curl -i -X POST "http://localhost:3000/hooks/sally" \
-H "Content-Type: application/json" \
--data @summary.json
Recommendation

Log the raw body of the first real delivery once and compare it with your parser. That way a field you rely on never surprises you in production.

5. On your side​

  • Answer quickly with 2xx and do the actual work afterwards, so slow processing does not block the delivery.
  • Make your handler idempotent, keyed on the summary id (recordingSummaryId), so a repeated delivery does no harm.
  • The payload contains the summary with its section items, in the same shape as in Get meeting summaries. One parser serves both paths.

6. Pause, change or remove a webhook​

POST/v1.0/directories/{directoryId}/webhooks/{webhookId}/deactivate pauses a webhook without deleting it. It stays in the list but does not fire until you activate it again with POST/v1.0/directories/{directoryId}/webhooks/{webhookId}/activate. Both calls take no request body and return the updated webhook.

# Pause
curl -X POST "https://api.sally.io/v1.0/directories/$DIRECTORY_ID/webhooks/$WEBHOOK_ID/deactivate" \
-H "Authorization: Bearer $SALLY_TOKEN"

# Resume
curl -X POST "https://api.sally.io/v1.0/directories/$DIRECTORY_ID/webhooks/$WEBHOOK_ID/activate" \
-H "Authorization: Bearer $SALLY_TOKEN"

After a deactivate, isActive is false:

200 OK
{
"webhookId": "af39409b-ca90-443d-a5e8-47dfd97c518f",
"name": "CRM sync",
"event": "summary.ready",
"url": "https://example.com/hooks/sally",
"isActive": false,
"directoryWide": false,
"createdOn": "2026-09-24T09:30:00Z"
}
Recommendation

While you rebuild your endpoint, deactivate the webhook instead of deleting it and activate it again afterwards.

To move a webhook to a new URL, rename it or switch its event, use PATCH/v1.0/directories/{directoryId}/webhooks/{webhookId}. Only the fields you send (url, event, name) change. Activating and deactivating is not done there but with the two calls above. DELETE/v1.0/directories/{directoryId}/webhooks/{webhookId} removes a webhook and answers 204.

7. Complete setup script​

This script makes sure exactly one webhook points at your URL: it looks for an existing webhook with that URL, activates it if it is paused, and registers a new one only if none exists. You can run it on every deployment.

#!/usr/bin/env bash
# Needs SALLY_TOKEN, DIRECTORY_ID and HOOK_URL, plus jq.
set -euo pipefail
BASE="https://api.sally.io/v1.0/directories/$DIRECTORY_ID"

existing=$(curl -s "$BASE/webhooks" -H "Authorization: Bearer $SALLY_TOKEN" \
| jq -c --arg url "$HOOK_URL" 'map(select(.url == $url)) | first // empty')

if [ -z "$existing" ]; then
curl -s -X POST "$BASE/webhooks" \
-H "Authorization: Bearer $SALLY_TOKEN" \
-H "Content-Type: application/json" \
-d "{\"url\": \"$HOOK_URL\", \"event\": \"summary.ready\", \"name\": \"CRM sync\"}" | jq .
elif [ "$(echo "$existing" | jq '.isActive')" = "false" ]; then
id=$(echo "$existing" | jq -r '.webhookId')
curl -s -X POST "$BASE/webhooks/$id/activate" -H "Authorization: Bearer $SALLY_TOKEN" | jq .
else
echo "$existing" | jq .
fi

8. Endpoints at a glance​

Each entry leads to the reference, where you can send the request with your own token.

For setting up webhooks in the Sally app without code, see the webhook integration guide.