Notify a service when items change
Declare a hook on a collection and Quilium calls your URL each time an item is created, updated or trashed. No code on the site, and the same call whichever door the change came through.
A hook tells another system that a collection changed: a CRM that wants every new request, a search index, a Slack relay, a rebuild trigger. You declare it in the collection's YAML. Quilium then sends the item as JSON to your URL, from its own servers, after the save.
Hooks exist on collections only, not on pages, blocks or global fields. To forward a form submission rather than react to an item, use the http action of the form instead: see custom fields.
1. Declare the hook
hooks is a top-level key of the collection. Each event takes a list, so one event can call several URLs.
requests:
name: Requests
objectName: Request
hooks:
created:
- type: http
config:
url: https://example.com/quilium/hook?token=a-long-random-value
updated:
- type: http
config:
url: https://example.com/quilium/hook?token=a-long-random-value
deleted:
- type: http
config:
url: https://example.com/quilium/hook?token=a-long-random-value
method: POST
tabsets:
# ...
| Key | Required | Notes |
|---|---|---|
hooks.created / updated / deleted |
no | A list of hooks. Declare only the events you need. |
type |
yes | http is the only type. |
config.url |
in practice | Not checked when the YAML is saved. Without it, the hook silently does nothing. |
config.method |
no | POST by default. POST, PUT and PATCH carry the JSON body; any other method sends none. |
You cannot set your own headers.
2. Know what sends what
The trigger is on the server, so the backoffice, the MCP, the CLI, the API, public form submissions and retention all send the same hooks.
| What happens to the item | Event sent |
|---|---|
| Created, duplicated, or received from a public form | created |
| Edited, one call per item in a bulk edit | updated |
Reordered by hand in a sortable collection |
updated, with empty before and requested |
| Moved to the trash by an editor | deleted |
| Moved to the trash by retention | deleted |
| Restored from the trash | created |
| Deleted permanently, from the trash or by the 30-day purge | nothing |
Three consequences for the receiver:
deletedmeans "trashed". The item can come back, and when it does you receivecreatedwith an id you already know. Write by id, so that a secondcreatedreplaces rather than duplicates.- You are not told when an item is finally purged. It already sent
deletedwhen it left the site, and it was offline from that moment. - A CSV import sends one call per row. Make sure the receiver can take the volume.
A few changes send nothing because they do not save the item: renaming or deleting a tag in the tag registry, and editing the collection's YAML.
3. Read the payload
The body is JSON. There is no Content-Type header, so a framework that only parses application/json will hand you an empty body. Read the raw body and decode it yourself. The request carries User-Agent: Quilium.io and Accept: application/json.
created
{
"action": "created",
"after": {
"id": "ceebd0ef-8008-4af5-9c30-cfe1a27ab720",
"title": "A",
"price": 10,
"created_at": "2026-11-04 10:58:52",
"updated_at": "2026-11-04 10:58:52"
}
}
updated
{
"action": "updated",
"before": { "id": "ceebd0ef-…", "title": "A", "price": 10 },
"requested": { "title": "B" },
"after": { "id": "ceebd0ef-…", "title": "B", "price": 10 },
"changes": { "title": "B" }
}
requested is what the save asked for, changes is what actually changed. before and after are shortened here: they hold the whole item.
deleted
{
"action": "deleted",
"before": { "id": "ceebd0ef-…", "title": "B", "price": 10 }
}
What the item contains:
- The root fields of the collection, in the default language. Translations are not included.
- Values as stored: a
tagfield holds uuids rather than labels, and relations are not resolved. If you need more, read the item back through the API with itsid. id,created_at,updated_at, in the CMS timezone without an offset.- Internal columns you should ignore, such as
searchand_order.deleted_atis not usable: do not read it to tell whether an item is trashed,actionalready says so.
4. Protect the endpoint
Quilium does not sign the request, and you cannot add an authorization header. The only secret available is the URL itself, hence the token in the example above.
- Use a long random token and compare it on your side.
- Use
https. - Treat the URL as a secret: it sits in plain text in the site's YAML, so it travels with the settings and with site exports.
- Do not trust the body for anything sensitive. Take the
idand read the item back through the API.
5. Plan for failures
The call runs in the queue, after the save. A failing hook never blocks or undoes the save, and the editor sees nothing.
| Your endpoint answers | What Quilium does |
|---|---|
2xx |
Done. |
4xx |
Gives up, no retry. |
5xx, a timeout (10 seconds) or a connection failure |
Tries again right away, three attempts in total, then gives up. |
Nothing keeps a failed call for later, so a long outage loses events. If that matters, reconcile from time to time by listing the collection. Since a retry can repeat a call and calls can arrive out of order, make the receiver idempotent: answer fast, queue the work on your side, and write by id.
6. Test it
No screen in the backoffice shows deliveries. Point the hook at a request inspector you control, then in the backoffice:
- Create an item. You should receive
created. - Edit it. You should receive
updated, with the field you changed inchanges. - Delete it. You should receive
deleted. - Restore it from the trash. You should receive
created. - Delete it, then delete it permanently from the trash. You should receive
deletedonce, and nothing for the permanent delete.
Then swap in the real URL.
Related
- Collection-type schema
- Custom fields, for the form
httpaction - Build a complete contact form
Last updated November 4, 2026.