Skip to content
Quilium Docs
Back to site
How-to guides

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:

  • deleted means "trashed". The item can come back, and when it does you receive created with an id you already know. Write by id, so that a second created replaces rather than duplicates.
  • You are not told when an item is finally purged. It already sent deleted when 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 tag field holds uuids rather than labels, and relations are not resolved. If you need more, read the item back through the API with its id.
  • id, created_at, updated_at, in the CMS timezone without an offset.
  • Internal columns you should ignore, such as search and _order. deleted_at is not usable: do not read it to tell whether an item is trashed, action already 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 id and 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:

  1. Create an item. You should receive created.
  2. Edit it. You should receive updated, with the field you changed in changes.
  3. Delete it. You should receive deleted.
  4. Restore it from the trash. You should receive created.
  5. Delete it, then delete it permanently from the trash. You should receive deleted once, and nothing for the permanent delete.

Then swap in the real URL.

Related


Last updated November 4, 2026.