Feel My Mac local API
Add trackpad feedback to a macOS interaction using small HTTP requests. Send a single impulse, follow physical movement, or drive a continuous effect from your own application’s state.
API v1 is available in Feel My Mac 1.2.0 or later. Detect capabilities at runtime; older public builds do not expose these endpoints. The version catalog records the documentation version and release status.
Access and permitted use
A purchased, activated Feel My Mac license is required to unlock the Local API material. There is no additional API usage fee for experimentation and nonprofit use. To integrate the APIs into a game or program, contact business@thehighjack.it to discuss the integration. This contact requirement is not limited to paid products.
The API Tester is an optional, separate download on the API & Docs page. It is not bundled with Feel My Mac and does not replace the licensed application. There is no SDK binary to embed: your macOS process talks to Feel My Mac locally.
Before your first request
You need macOS 15.1 or later, a compatible haptic trackpad, and a Feel My Mac build that supports this API. In that app, select Local API in the material selector and enable Haptic feedback. Leave that one instance of Feel My Mac running while your client uses the API.
The service listens at http://127.0.0.1:59723 only while Local API is selected.
Changing material closes the service. Its status reports whether output is
currently available; a successful connection alone does not prove that the
trackpad can emit feedback.
The material sound is disabled in API mode. Independent click and keyboard sounds retain their own settings. API commands never enable those sounds.
Quickstart
Check the service without producing feedback:
curl --fail-with-body http://127.0.0.1:59723/v1/status
curl --fail-with-body http://127.0.0.1:59723/v1/parameters
Look for outputAvailable: true in status. The parameter response lists
supported modes, controls and limits. Check that resonance.controlled exists
before using app-driven resonance. There is no need to poll either endpoint
while your client is idle.
Generate one UUID per running client instance, then reuse it for every command, heartbeat and stop:
CLIENT_ID=$(uuidgen)
curl --fail-with-body http://127.0.0.1:59723/v1/impulses \
-H 'Content-Type: application/json' \
-H "X-FMM-Client: $CLIENT_ID" \
-H 'X-FMM-Client-Name: My integration' \
--data '{"style":"natural","intensity":0.25}'
This emits one gentle test impulse if the device and output policy permit it.
emitted: false means no impulse was sent. ok: true means the command was
accepted; it does not guarantee a particular sensation.
No token or per-app authorization is required. X-FMM-Client coordinates
ownership of an effect; it is not a credential or proof of an app’s identity.
Any local process that can reach the service can submit commands while the
material is selected. Another UUID receives 409 busy while a session is
owned. The same client can replace its own effect.
Choose the interaction
| You want to feel | Use | How it ends |
|---|---|---|
| A click, collision or confirmation | POST /v1/impulses |
One impulse |
| A short vibration at a fixed cadence | mode: "timed" |
20–2000 ms from its original start |
| Feedback following physical trackpad motion | mode: "gesture" |
Explicit stop, or 2 seconds without renewal |
| Tension or another value computed by your app | mode: "controlled" |
Explicit stop, or 2 seconds without renewal |
| A small pattern of separate impacts | POST /v1/sequences |
Last step, explicit stop, or replacement |
Gesture mode requires valid contact and movement from the selected trackpad. Controlled mode follows your values independently of physical movement; your app decides what should happen when the user stays still.
Drive an effect from your app
Start a controlled session:
{
"mode": "controlled",
"intensity": 0.10,
"pulseRateHz": 6,
"sharpness": 0,
"transitionMs": 60
}
Send that object to POST /v1/resonance. Start a heartbeat only while this
interaction is active: send POST /v1/heartbeat with {} every 500 ms. The
server expires the session 2 seconds after its most recent accepted renewal.
A controlled session can otherwise last as long as the interaction needs.
During the effect, send only changed values to POST /v1/resonance/update:
{"intensity":0.75,"pulseRateHz":50,"sharpness":1}
intensityis normalized from 0 to 1 and respects the user’s API intensity limit. Zero keeps a silent session that can be raised again.pulseRateHzrequests 1–60 impulses per second.sharpnessselects a character from soft (0) to crisp (1), using eight prepared impact profiles. It is not a physical-frequency control.transitionMssets the time to approach new intensity, cadence and sharpness values. Its default is 40 ms, range 0–1000 ms. Omitted updates keep the previous setting. A new transition begins at the current interpolated value.
Initial values apply directly at start. To fade in, start at intensity zero, then update intensity with a nonzero transition.
The API does not expose the physical frequency of the actuator. Status and
parameters report actuatorFrequencyControl: false. A frequencyHz field is
rejected. Actual cadence also depends on scheduling and native output limits.
Example: draw back a billiards cue
Calculate a charge p between 0 and 1 from the cue’s distance. Map it to a
curve, then interpolate the three controls:
const t = Math.pow(Math.max(0, Math.min(1, p)), 1.7);
const values = {
intensity: 0.10 + (0.75 - 0.10) * t,
pulseRateHz: 6 + (50 - 6) * t,
sharpness: t
};
The exponent and endpoints belong to your game. The server does not impose a billiards-specific effect. The same approach can follow a stretched bowstring, a spring, a scrubber position or the pressure of a drawing tool.
While the cue stays still, choose the behavior your interaction needs:
- Keep the current values to maintain tension.
- Reduce intensity for a quieter held state.
- Send
POST /v1/resonance/pausewith{}and later/v1/resonance/resumewith{}to suspend and resume emission in the same session.
Keep renewing even while paused. Pause stops emission; transitions continue to advance with time. You may edit parameters in pause, and resume uses the value reached at that time. It does not replay missed pulses.
At release, send POST /v1/stop with {}. After its response, you may send a
single crisp impulse to represent the impact. On cancellation, loss of focus
or closing your interaction, send only stop. A gesture that was cancelled should
not produce its planned release impact.
Keep the client small and responsive
Use one request in flight per client and keep only the newest pending values. Coalesce pointer or game-frame updates to at most 20 requests per second; you do not need to send one request per haptic pulse. Feel My Mac schedules the pulses locally. Accepted updates, pause and resume renew a controlled session; a heartbeat covers periods without changes.
Stop takes priority over queued edits. If the interaction ends while start is still in flight, let that request finish and send stop afterward. Preserve the same client UUID and server address for that stop. If the request outcome is unknown after a timeout, attempt a bounded stop; do not automatically retry an impulse that might already have happened. The lease provides a final timeout for renewable sessions.
On macOS, timers supporting an interaction with a held click must run in common run-loop modes. A timer registered only in the default mode can pause during AppKit event tracking and let a session expire. Do not leave heartbeat timers running after stop, and do not automatically restart an expired effect unless the interaction is still wanted.
Try the native tester
The API & Docs page offers the tester as a separate ZIP download. Use the compatibility and availability information beside that download to choose the current version.
- Download the tester ZIP and double-click it in Finder to extract it.
- Open Feel My Mac API Tester.app. Keep your licensed Feel My Mac runtime running with Local API selected and haptic output enabled.
- The tester connects once on launch. If it shows Da collegare, press Verifica after the runtime is ready.
- In the Risonanza sidebar section, choose Controllata, press Avvia, adjust the controls, then try Pausa and Riprendi. Open Biliardo after stopping to compare maintained tension with Pausa da fermo.
- Press Ferma tutto or Escape to stop. Losing focus or closing the tester also stops its interaction.
No terminal command is needed to use the native tester. It sends HTTP requests to the same API your integration will use; it does not contain the haptic engine or unlock the API without a purchased app license.
Practical examples
These examples use system or language standard libraries. By default they run
without network requests or haptic output; --run explicitly enables local
requests.
| Example | Dry run | Connect to Feel My Mac |
|---|---|---|
| Python: controlled tension | python3 controlled.py |
python3 controlled.py --run |
| Node.js: controlled tension | node controlled.mjs |
node controlled.mjs --run |
| curl: one impulse | sh impulse.sh |
sh impulse.sh --run |
The Python and Node examples show capability discovery, serial requests, live
values, renewal while held, pause, editing during pause, resume and cleanup in
finally. They intentionally stop on the first failure instead of building an
unbounded retry queue. Their own demo loop ends; they do not install a daemon.
In an Electron application, use Node’s HTTP client in the main process.
A browser page is not an API client: requests containing Origin or
Sec-Fetch-* are refused. The website’s visual example does not connect to your
trackpad. Download the native tester to compare the actual sensations.
Handle changes and failures
Use /v1/parameters when connecting to discover the controls your installed
runtime supports. Read outputAvailable before starting. Keep API protocol,
documentation and tester versions separate in your integration notes.
For 409 busy, wait for the other interaction to end. For 429 rate_limited,
reduce the command rate and keep the newest values instead of replaying a queue.
For 503 output_unavailable, stop your local interaction and wait for the user
to restore output. A closed service normally produces a connection error rather
than JSON. Full details are in the endpoint reference and
machine-readable OpenAPI document.