Skip to main content

I Built the FileMaker Webhook Manager I Wished Existed — Here’s What I Learned #

The open-source tool, the architecture decisions, and the seven undocumented behaviours that nearly broke me #


A few weeks ago I wrote about how FileMaker Server 22.0.4’s OData webhooks turn the platform into a real event-driven citizen — Stripe-style, GitHub-style, properly modern. I still believe that. The capability is genuinely a turning point.

But then I sat down to build something real on top of it. A webhook manager. A tool that lets you browse your FileMaker databases, create and edit webhooks visually, test them without touching production data, and inspect the state of every pending delivery.

What I found in the process was a gap — between the feature exists and the feature is actually usable — that is wider than any official documentation lets on.

This is the follow-up article. The unflattering one. The one that, hopefully, saves you a weekend.


The Tool: FMS-odata-webhooks #

First, let me show you what I built, because it’s the context for everything else.

FMS-odata-webhooks is a React web application that gives you a visual interface to the FileMaker OData webhook API. It’s open source, MIT licensed, and built to handle all the quirks described below so you don’t have to rediscover them yourself.

What it does #

Database Browser — Connect to any FileMaker Server 22.0.4+ instance with your OData credentials. The app lists your available databases, lets you select one, and exposes the full table and field metadata, including FileMaker-specific type annotations. You can explore your schema before writing a single line of integration code.

Webhook Manager — A full CRUD interface for webhook management:

  • Create webhooks with visual field selection from the live schema
  • Configure OData filter expressions with real-time validation
  • Set custom HTTP headers (for receiver authentication, metadata, or routing)
  • Toggle schema-change notifications per webhook
  • Edit webhooks — which abstracts the delete+create pattern the API requires (more on this below)
  • Delete webhooks with confirmation
  • Manually invoke any webhook against specific record IDs for testing

Pending Operations Panel (upcoming) — Every webhook carries a pendingOperations[] queue of events waiting to be delivered, or that have failed. The upcoming panel will expose this queue directly in the UI: operation type (ADD, UPDATE, DELETE, SCHEMA), delivery status, error codes, error messages, and retry count. This information is in the API response but completely absent from the official documentation. Surfacing it is on the roadmap, because it is gold for debugging.

Endpoint Testing — A suite of browser-console utilities (window.FileMakerTests.*) for low-level API exploration: connectivity checks, full endpoint validation across all HTTP methods, integration tests that create and clean up real webhooks.

Tech stack #

React 18 + Vite, Tailwind CSS, shadcn/ui components. No backend, no database. All state lives on FileMaker Server. In development, Vite proxies /fmi/* to your server and handles CORS. In production, you need nginx to do the same — which is one of the undocumented behaviours covered below.

The code is at github.com/fsans/FMS-odata-webhooks. Fork it, use it, open issues.


Getting Started in Five Minutes #

git clone https://github.com/fsans/FMS-odata-webhooks.git
cd FMS-odata-webhooks
npm install
npm run dev

Open http://localhost:5173. Before connecting, visit https://your-filemaker-server/fmi/odata/v4 in your browser and accept the SSL certificate warning — this unblocks the dev-mode proxy. Enter your server host and OData credentials, click Connect.

That’s it. You’re browsing your FileMaker schema and managing webhooks.


The Seven Things Nobody Told Me #

Now for the part that cost the most time. These are not edge cases. They are fundamental behaviours of the FileMaker OData webhook API that you will hit in any serious integration project. None of them are documented in the Claris OData guide.

1. “POST-only” is a myth — the real contract is mixed GET/POST #

Public information on this API is sparse — close to none. A minimal post on Claris Community from Claris tech staff, and briefly my own README, said the same thing: all webhook operations are POST-only.

Wrong.

Per the official Claris OData guide and confirmed through systematic testing, the actual HTTP contract is:

EndpointMethodNotes
Webhook.GetAllGETReturns full list
Webhook.Get(<id>)GET<id> in URL path
Webhook.AddPOSTConfig in JSON body
Webhook.Delete(<id>)POST<id> in URL path
Webhook.Invoke(<id>)POST<id> in URL path, rowIDs in body

Read endpoints are GET. Action endpoints are POST. And the <id> is an OData function argument in the URL path — not a webhookID field in the JSON body. If you get this wrong you get HTTP 400 responses with no useful error message. The app handles this correctly; I’m documenting it here because you’ll encounter it the moment you try to call the API directly.

  • ❌ PUT and PATCH do not exist on any webhook endpoint.
  • ❌ REST-style DELETE /Webhook/<id> does not exist either. Use POST /Webhook.Delete(<id>).

2. The id field trap — and why it can freeze your webhook permanently #

This one has a sharp edge.

The lowercase string id is treated as an internally reserved word by FileMaker’s OData engine. It maps to the internal FileMaker record ID. If you have a field in your table also named id, you get a naming collision — and the collision doesn’t just fail gracefully.

Here is a real error from my test environment, captured directly from the pendingOperations queue:

{
  "operation": "UPDATE",
  "rowIDs": [19972],
  "status": "NOT_SENT",
  "lastErrorCode": -1002,
  "lastErrorMessage": "Error: syntax error in URL at: 'id'",
  "sendAttempts": 36
}

FileMaker keeps retrying. In my test environment sendAttempts climbed past tens of thousands with no resolution and the queue never cleared on its own — the webhook is effectively frozen until you delete it.

The fix:

  • Best: rename the field. ID (uppercase) does not collide. Neither does record_id, row_id, contact_id, or anything that isn’t the exact lowercase string id.
  • Quick workaround: quote it in $select — use "id" instead of id.
$select=first_name,last_name,"id"   ✅
$select=first_name,last_name,id     ❌ (syntax error in URL)

The app validates field selections and warns you if a field named id is included unquoted.

3. No PATCH, no PUT — every edit is a deletion #

Want to tighten a webhook’s filter? Adjust which fields it includes? Change the callback URL?

You cannot. There is no Webhook.Update. There is no PATCH. There is no soft edit of any kind.

The only way to modify a webhook is to delete it and create a new one. The new webhook gets a fresh, sequential integer ID. Every external system that stored the old ID — your receiver’s routing logic, your monitoring dashboard, your documentation — now references a dead webhook.

This is the underlying contract. The app abstracts it: the Edit dialog performs delete+create transparently and shows you the new ID. But you need to know this is happening, because the ID change has real downstream consequences in production.

A practical tip: store webhook IDs in your receiver’s configuration as environment variables, not hardcoded. That way a delete+create cycle is a config update, not a code deploy.

4. The pendingOperations queue: your best debugging tool, entirely undocumented #

Every webhook returned by Webhook.GetAll carries a pendingOperations[] array. Each entry describes an event that is queued for delivery or has failed:

{
  "operation": "UPDATE",
  "rowIDs": [19967],
  "status": "NOT_SENT",
  "lastErrorCode": 0,
  "lastErrorMessage": "",
  "sendAttempts": 0
}

Operations can be ADD, UPDATE, DELETE, or SCHEMA. The status field, lastErrorCode, lastErrorMessage, and sendAttempts tell you exactly what FileMaker tried, when it failed, and how many times it has retried.

This is the most useful debugging surface in the entire API. It’s also nowhere in the public documentation. You discover it by reading a raw API response.

When the Pending Operations Panel ships, the app will show it inline for every webhook. Until then, you can inspect the queue by reading the raw Webhook.GetAll response — if a webhook is silently failing, this is where you look first.

5. The receiver payload schema is undocumented — you reverse-engineer it #

What does FileMaker actually POST to your webhook URL when a record changes?

There is no documented schema. You find out by setting up a receiver endpoint, logging whatever arrives, and reverse-engineering the structure.

From my testing, a content-change payload looks like this:

{
  "@odata.context": "fmi/odata/v4/Contacts/$metadata#contact(\"uuid\",\"mod_id\",\"row_id\")",
  "value": [
    {
      "@odata.editLink": "fmi/odata/v4/Contacts/contact(3276,...)",
      "@odata.id": "fmi/odata/v4/Contacts/contact(3276,...)",
      "mod_id": 0,
      "row_id": 19071,
      "uuid": "211EE2A8-D2C4-4F14-AA4A-A4B05E60C8FE"
    }
  ]
}

A schema-change notification looks entirely different:

{
  "@odata.context": "fmi/odata/v4/Contacts/$metadata#FileMaker_Tables(...)",
  "value": [
    {
      "BaseTableName": "contact",
      "ModCount": 32,
      "TableId": 1065106,
      "TableName": "contact"
    }
  ]
}

Two important observations:

  • The mod_id is 0 on manually invoked webhooks (Webhook.Invoke). No data actually changed, so there is no modification ID to report. Your receiver needs to handle this case.
  • The payload only contains the fields listed in $select. If you want the record’s content, you must either include those fields in $select at webhook creation time, or use the rowIDs from the payload to fetch the full record via a separate OData GET.

Verified sample payloads are documented in DISCOVERINGS.md in the repository.

6. Self-signed certs, CORS, and the nginx proxy you didn’t budget for #

FileMaker Server speaks HTTPS using a self-signed certificate by default, and it emits no CORS headers. This combination means a browser-based application cannot talk to it directly in production — the browser will refuse the request before it even leaves the machine.

The architecture that works:

Browser → nginx (your web server)
           ├── /fmwebhooks/*  → serves the React app (dist/)
           └── /fmi/*         → proxy_pass to FileMaker Server
                                 (proxy_ssl_verify off)

nginx terminates TLS for your public domain, and proxies /fmi/* to FileMaker Server with proxy_ssl_verify off to accept FMS’s self-signed cert. The frontend uses relative URLs (/fmi/odata/v4/...) throughout, so the same client code works in dev (Vite proxy) and production (nginx proxy) without modification.

The repository includes a ready-to-use nginx site config at docs/servers_enabled/fmwebhooks.conf and a standalone reverse-proxy snippet at nginx-reverse-proxy.conf.

This is an architecture decision you will have to make. The docs never warn you.

7. The good surprise: webhooks survive server restarts #

Here is the one that went the right way.

FileMaker Server persists webhook configurations internally. If you restart FMS — planned maintenance, unexpected reboot, update cycle — your webhooks come back. Webhook.GetAll returns them exactly as configured.

This is not documented anywhere. I confirmed it through deliberate testing. For production deployments, it means you don’t need to re-register webhooks after every server event. The configurations are durable.

The bias, for once, is in your favour.


An Honest Read on the Current State #

This feels like a feature that shipped capability-complete and documentation-incomplete.

The engine works. The OData contract is internally consistent once you reverse-engineer it. The 22.0.4 surface — filter expressions, notifySchemaChanges, manual Invoke, the pendingOperations queue — is genuinely well-designed. Someone at Claris thought carefully about these features.

But the developer experience around it is: read the spec, build a sandbox, watch what comes back, write your own docs. Trial, error, and a healthy network tab.

I turned that experience into an open-source tool so you don’t have to repeat it.


What I’d Love to See in 22.0.5 or 23.0 #

A short wishlist for Claris, in priority order:

  • A documented receiver payload schema. Just publish what FMS sends for each operation type. One page. It would save every developer who touches this API several hours.
  • A native Webhook.Update(<id>) so edits don’t break external ID references.
  • Stable webhook IDs across edits, or at minimum a user-supplied stable key field.
  • HMAC signing of outbound webhooks so receivers can verify provenance.
  • A documented retry policy and a maxFailedAttempts story that is visible end-to-end.
  • Script enumeration via $metadata — today there is no documented way to list the scripts an OData account can call.
  • Meaningful lastErrorCode semantics — the current opaque integer needs a published key.

None of this is fundamental architecture work. It is developer-experience polish that turns a powerful capability into something people deploy without reading two hundred forum threads first.


The TL;DR #

The seven things to remember:

  • Read endpoints are GET, action endpoints are POST. The <id> goes in the URL path.
  • Never put unquoted id in $select — it freezes the webhook.
  • Every edit is a delete + create; IDs change.
  • The pendingOperations queue is your debugger.
  • The receiver payload schema is undocumented — log first, parse second.
  • Production needs an nginx reverse proxy for /fmi/*.
  • Webhooks survive server restarts.

Open-source webhook manager: github.com/fsans/FMS-odata-webhooks. Star it if it saves you a weekend.

If you have hit your own undocumented FileMaker OData behaviour, drop it in the comments. The collective bug list is, increasingly, the documentation.


This is Part 2 of a series on FileMaker Server OData webhooks. Part 1 covers the feature announcement and what it means for FileMaker’s position in modern architectures.


Further Reading: