Using the webhooks module

As some of you know I’m writing an “integration / extension” app in Python; more to come on that in a later post. I’m maintaining a separate DB for my app which I keep in sync with the farmOS system.

This led to a need for being notified when changes are made in farmOS proper, for real-time sync. @mstenta suggested the webhooks module, which I have since tested and it does in fact work for my needs. This post is to document various findings along the way.

Pros

It’s a normal Drupal module, just install and configure like the rest.

It (mostly) does what you’d expect: It will send a POST request to the webhook URI, on certain events (e.g. create/update/delete asset) - per your configuration.

The request body/payload contains “all” (?) data submitted by the user. (I’m not 100% sure since I did not wind up using this data per se.)

Cons

The webhook POST happens before the transaction is committed on the Drupal side. This can lead to race condition if the webhook receiver logic tries to fetch the record via JSONAPI, since it may not exist yet (or is stale; does not reflect newly-submitted values). So the receiver must either “delay” its JSONAPI fetch, or else parse request body to get the data.

While the POST body/payload does contain new data, it’s in a different format compared to the JSONAPI. This means the receiving app may need 2 separate parsers for each record type (asset/log etc.) - one for JSONAPI and one for webhooks.

Not exactly a con, but worth mentioning: Multiple webhooks may fire in quick succession, e.g. if user in farmOS creates a new log with material quantity which references a new material type and new units term - there will be 4 webhook requests sent out. So again on the receiving side, logic may need to avoid processing these in “parallel” since e.g. it can’t create the log until it has first created the unit + material type + quantity. Note however, the webhook request do appear to come in the correct order - i.e. 1) unit, 2) material type, 3) material quantity, 4) log.

If the webhook URI is not reachable, e.g. the domain name is valid but the remote (receiving) host is turned off, then behavior becomes quite bad. In this case, when you edit an asset in farmOS and click save, the browser request will appear to “stall” while the server waits for connection timeout - and it appeared as though it waits for 2 minutes - twice in a row (so 4 mins total) before response finally comes back to browser.

Also maybe not a con but worth mentioning: The project site shows a warning:

Minimally maintained
Maintainers monitor issues, but fast responses are not guaranteed.

Last release was May 2025 (IIUC) and I’m not sure if that’s “bad” per se - I don’t think so?

Possibly related (?), when configuring a webhook there are fields for “Secret” and “Token” - which presumably are ways to lock things down such that your webhook receiver can authenticate the requests. But in practice I could not tell that the requests included the secret/token - although I may not be understanding something…

Receiver Implementation Notes

For my own app I decided to handle (some of) the “cons” as follows:

When a webhook request comes in, I save minimal details (entity type, bundle, uuid, event type) to a change queue table in my DB. Then a separate daemon will process the queue. It basically polls the table every 1 second and when changes appear, it imports or deletes records as needed (using the JSONAPI to fetch data). This means:

  • webhook logic is “quick” - just add a stub record to change table
  • processing changes can then be done in a FIFO manner
  • re-fetching from JSONAPI lets me maintain just 1 record parser

The time between user clicking submit in farmOS, and the record being fully synced to my app, is somewhere between 0-2 seconds. So far in practice the JSONAPI fetch is “delayed” enough that it’s not getting stale data; I may need to tweak timing more if that becomes an issue.

Webhook Events

The webhooks module lets you choose from several event types, for which you want a webhook to fire. In practice I defined only one webhook, and enabled these event types for it:

  • entity:asset:create
  • entity:asset:update
  • entity:asset:delete
  • entity:log:create
  • entity:log:update
  • entity:log:delete
  • entity:quantity:create
  • entity:quantity:update
  • entity:quantity:delete
  • entity:taxonomy_term:create
  • entity:taxonomy_term:update
  • entity:taxonomy_term:delete

Sample Request

These are raw values from a sample webhook request in my local dev environment (where my app runs on the wuttafarm-local.edbob.org domain and farmos-local.edbob.org is the farmOS instance).

Headers:

Host: wuttafarm-local.edbob.org
User-Agent: Drupal/11.3.5 (+https://www.drupal.org/) GuzzleHttp/7
X-Drupal-Event: entity:asset:update
Content-Type: application/json
X-Drupal-Delivery: eb770f19-2496-44e5-ad34-9677bd5b966c
X-Hub-Signature-256: sha256=6738c766ae4247782d5d27f81bcc74e852a3e1dadc8dd6452791c0bf46a743a2
X-Hub-Signature: sha1=d8b51cc921bdcb9568e694997b2c1be183332d8c
X-Forwarded-Proto: https
X-Forwarded-For: 192.168.44.105
X-Forwarded-Host: wuttafarm-local.edbob.org
X-Forwarded-Server: wuttafarm-local.edbob.org
Content-Length: 3361
Connection: Keep-Alive

GET/POST Params

(none)

Body (JSON)

{
  "event": "entity:asset:update",
  "user": {
    "uid": [
      {
        "value": 1
      }
    ],
    "uuid": [
      {
        "value": "0000bce1-5ae4-414c-ba27-ea0bd90762ca"
      }
    ],
    "langcode": [
      {
        "value": "en"
      }
    ],
    "preferred_langcode": [
      {
        "value": "en"
      }
    ],
    "preferred_admin_langcode": [],
    "name": [
      {
        "value": "admin"
      }
    ],
    "mail": [
      {
        "value": "farmos@edbob.org"
      }
    ],
    "timezone": [
      {
        "value": "America/Chicago"
      }
    ],
    "status": [
      {
        "value": true
      }
    ],
    "created": [
      {
        "value": "2026-01-20T21:30:59+00:00",
        "format": "Y-m-d\\TH:i:sP"
      }
    ],
    "changed": [
      {
        "value": "2026-02-18T16:26:56+00:00",
        "format": "Y-m-d\\TH:i:sP"
      }
    ],
    "access": [
      {
        "value": "2026-03-11T15:23:17+00:00",
        "format": "Y-m-d\\TH:i:sP"
      }
    ],
    "login": [
      {
        "value": "2026-03-11T14:13:22+00:00",
        "format": "Y-m-d\\TH:i:sP"
      }
    ],
    "init": [
      {
        "value": "lance@edbob.org"
      }
    ],
    "roles": [],
    "default_langcode": [
      {
        "value": true
      }
    ]
  },
  "entity": {
    "id": [
      {
        "value": 9
      }
    ],
    "uuid": [
      {
        "value": "7177a99b-19b3-4f20-96a4-c4f2c6f88e65"
      }
    ],
    "revision_id": [
      {
        "value": 66
      }
    ],
    "langcode": [
      {
        "value": "en"
      }
    ],
    "type": [
      {
        "target_id": "animal",
        "target_type": "asset_type",
        "target_uuid": "d9a2b729-5a60-476b-805d-d68955d16013",
        "url": "/admin/structure/asset-type/animal"
      }
    ],
    "revision_created": [
      {
        "value": "2026-03-11T15:23:18+00:00",
        "format": "Y-m-d\\TH:i:sP"
      }
    ],
    "revision_user": [
      {
        "target_id": 1,
        "target_type": "user",
        "target_uuid": "0000bce1-5ae4-414c-ba27-ea0bd90762ca",
        "url": "/user/1"
      }
    ],
    "revision_log_message": [],
    "uid": [
      {
        "target_id": 1,
        "target_type": "user",
        "target_uuid": "0000bce1-5ae4-414c-ba27-ea0bd90762ca",
        "url": "/user/1"
      }
    ],
    "name": [
      {
        "value": "Ivory"
      }
    ],
    "created": [
      {
        "value": "2026-01-24T22:49:35+00:00",
        "format": "Y-m-d\\TH:i:sP"
      }
    ],
    "changed": [
      {
        "value": "2026-03-11T15:23:18+00:00",
        "format": "Y-m-d\\TH:i:sP"
      }
    ],
    "archived": [
      {
        "value": false
      }
    ],
    "last_archived": [],
    "default_langcode": [
      {
        "value": true
      }
    ],
    "revision_translation_affected": [
      {
        "value": true
      }
    ],
    "data": [],
    "file": [],
    "image": [
      {
        "target_id": 3,
        "alt": "",
        "title": "",
        "width": 4096,
        "height": 3072,
        "target_type": "file",
        "target_uuid": "02037845-249b-4400-b9fa-59bd1bf50de5",
        "url": "https://farmos-local.edbob.org/system/files/farm/asset/2026-01/IMG_20260107_172500.jpg"
      }
    ],
    "notes": [
      {
        "value": "birthdate is rough estimate; need to get a better one from Annabelle?",
        "format": "default",
        "processed": "<p>birthdate is rough estimate; need to get a better one from Annabelle?</p>\n"
      }
    ],
    "flag": [],
    "group": [],
    "id_tag": [],
    "location": [
      {
        "target_id": 2,
        "target_type": "asset",
        "target_uuid": "43e0aa67-4139-47ce-8178-cb050f10fb81",
        "url": "/asset/2"
      }
    ],
    "geometry": [
      {
        "value": "POLYGON ((-91.82583768690577 37.63767873713154, -91.8257959699464 37.63761634927293, -91.82586618068468 37.63758662301339, -91.82590823761205 37.637650754651105, -91.82583768690577 37.63767873713154))",
        "geo_type": "Polygon",
        "lat": 37.6376330117284,
        "lon": -91.82585212432815,
        "left": -91.82590823761205,
        "top": 37.63767873713154,
        "right": -91.8257959699464,
        "bottom": 37.63758662301339,
        "geohash": "9ywwn7w",
        "latlon": "37.637633011728,-91.825852124328"
      }
    ],
    "intrinsic_geometry": [],
    "is_location": [
      {
        "value": false
      }
    ],
    "is_fixed": [
      {
        "value": false
      }
    ],
    "owner": [
      {
        "target_id": 2,
        "target_type": "user",
        "target_uuid": "32188d6c-4b18-46ce-961d-7c1c49d320e1",
        "url": "/user/2"
      }
    ],
    "parent": [],
    "quick": [],
    "produces_eggs": [
      {
        "value": false
      }
    ],
    "animal_type": [
      {
        "target_id": 1,
        "target_type": "taxonomy_term",
        "target_uuid": "17b4ce65-880d-461f-8d07-547612a82ad1",
        "url": "/taxonomy/term/1"
      }
    ],
    "birthdate": [
      {
        "value": "2024-01-01T06:00:00+00:00",
        "format": "Y-m-d\\TH:i:sP"
      }
    ],
    "is_sterile": [
      {
        "value": true
      }
    ],
    "nickname": [],
    "sex": [
      {
        "value": "F"
      }
    ]
  }
}

Screenshots

Not sure how helpful, but figured I might as well add these..

Module installation - note that you need the “Webhooks” module but not “Webhook” (which is for something else - but they both come with the ‘drupal/webhooks’ package).

Defining a webhook (part 1):

Defining a webhook (part 2):

Defined webhooks listing:

Other Links

@mstenta also shared this link which describes how to define your own webhooks in Drupal/PHP. I’m still personally intimidated by the ecosystem so I was glad the webhooks module did what I needed.

Project issue re: Webhooks triggered before database transaction ends - possibly this will someday improve the transaction vs. webhook timing, although in my case the separate processing of FIFO queue is needed anyway, and obviates the problem somewhat.

1 Like

Thanks for the detailed writeup @ledgar! And welcome to the forum! :smile:

That’s a bummer about the implemented webhooks blocking i/o on the drupal request/response lifecycle (rather than queuing in the background), and also the bit about firing ahead of a committed transaction (uncertain guarantee)…

Is your extension/python app database an altogether different schema than Drupal’s, just having the same logical objects? Granting that Drupal’s own node & node_revisions schema and entity system is about as abstract a modeling or ORM system as can be imagined.

2 Likes

Yeah, bummer about the I/O blocking especially. My use case is small scale and likely won’t be a real problem..we’ll see.

My schema is different, just same logical objects. It’s a subset and not sure yet how much that will grow. I have some ideas for extending it on my side but haven’t got that far yet.

https://forgejo.wuttaproject.org/wutta/wuttafarm/src/branch/master/src/wuttafarm/db/model

I haven’t done much PHP in many years and am totally new to Drupal, so haven’t honestly looked into their model code - just relying on the JSONAPI and farmOS docs to learn schema details needed.

1 Like