Local API v1 reference
Available in Feel My Mac 1.2.0 or later. All endpoints use the base URL
http://127.0.0.1:59723. See the integration guide for prerequisites,
usage policy and practical examples, or download the OpenAPI document.
Transport
| Property | Contract |
|---|---|
| Protocol | HTTP/1.1, JSON responses, one request per connection |
| Authentication | None; no token or Authorization header is required |
| Client ownership | Every POST requires X-FMM-Client, a UUID reused by that client instance |
| Client label | Optional ASCII X-FMM-Client-Name; displayed label is limited to 64 characters |
| POST body | Content-Type: application/json and an exact Content-Length |
| Empty commands | Send {} for heartbeat, stop, pause and resume |
| GET body | None |
| Host | 127.0.0.1:59723 or localhost:59723; connect through IPv4 loopback |
| Request paths | Exact paths below, without query parameters or fragments |
| Browser requests | Origin and any Sec-Fetch-* header are refused |
| Size limits | 8 KiB headers, 16 KiB body, 24 KiB complete request |
| Connections | At most 8 simultaneous connections; incomplete requests time out after 3 seconds |
| Unsupported framing | Chunking, compression, duplicate headers, pipelining and Expect |
The service exists only while the licensed Local API material is selected.
Client UUIDs coordinate effect ownership; they do not authenticate processes.
Responses close the connection and use Cache-Control: no-store.
Endpoints
| Method | Path | Purpose |
|---|---|---|
| GET | /v1/status |
Runtime availability and current effect state |
| GET | /v1/parameters |
Supported controls, bounds and interaction rules |
| POST | /v1/impulses |
One impact |
| POST | /v1/resonance |
Start or replace a resonance session |
| POST | /v1/resonance/update |
Change values in the current session |
| POST | /v1/resonance/pause |
Pause controlled emission |
| POST | /v1/resonance/resume |
Resume controlled emission |
| POST | /v1/sequences |
A bounded sequence of impacts |
| POST | /v1/heartbeat |
Renew a gesture or controlled session |
| POST | /v1/stop |
Stop the current client’s effect |
GET /v1/status
Does not produce feedback and does not require a client UUID. Example:
{
"ok": true,
"version": 1,
"authentication": "none",
"mode": "localAPI",
"hapticEnabled": true,
"customActuationAvailable": true,
"gestureAvailable": true,
"styles": ["crisp", "natural", "soft"],
"outputAvailable": true,
"materialAudioEnabled": false,
"activeEffect": "none",
"resonance": null,
"remainingDurationMs": 0,
"intensityLimit": 1,
"gestureLeaseMs": 2000,
"controlledLeaseMs": 2000,
"maxTimedDurationMs": 2000,
"actuatorFrequencyControl": false
}
outputAvailable is the current permission to emit, not a guarantee of a
successful hardware command. gestureAvailable describes contact capability;
starting the monitor can still fail. activeEffect is none, gesture,
timed, controlled or sequence.
Resonance start/update and pause/resume responses include the same effect state:
resonance: requested values for the session, ornullif absent.remainingDurationMs: remaining time for timed resonance; zero when no resonance session exists. Omitted for gesture and controlled modes.paused: present for controlled mode.currentValues: controlled intensity,pulseRateHzandsharpnessat the current transition position, before the user’s intensity multiplier.
GET /v1/parameters
Does not produce feedback and does not require a client UUID. Returns the
impulses, resonance.gesture, resonance.timed, resonance.controlled,
resonanceUpdate, resonancePause, resonanceResume and sequences descriptions.
Each control declares its type and bounds or enum. It also declares 500 ms
recommended heartbeats, 2000 ms renewable leases, 20 recommended updates/s,
60 maximum effect commands/s, authentication: "none",
materialAudioEnabled: false and actuatorFrequencyControl: false.
Treat this response as capability discovery. A client should check for the mode and controls it needs instead of inferring them from an app version string.
POST /v1/impulses
{"style":"crisp","intensity":0.25}
| Field | Required | Values | Meaning |
|---|---|---|---|
style |
Yes | crisp, natural, soft |
Nominal impact profiles of 2, 3.8 and 6 ms |
intensity |
Yes | Number 0–1 | Normalized strength; zero is silent |
Response: {"ok":true,"emitted":true}. emitted: false means no impact was
sent, for example because intensity is zero, the user’s limit is zero, a native
rate limit suppressed it, or hardware output failed. The command replaces an
effect owned by the same UUID and does not retain ownership after completion.
The profile durations describe commands sent to the actuator, not measured physical response or a programmable actuator frequency.
POST /v1/resonance
Start a new session or replace your current effect. The mode is required and
cannot be changed by an update. Send exactly the applicable fields; unknown
fields and null values are rejected for resonance starts and updates.
All numbers must be finite JSON numbers, not strings or booleans.
Gesture mode
{"mode":"gesture","intensity":0.4,"fineness":0.5,"reactivity":0.5}
| Field | Required | Range | Meaning |
|---|---|---|---|
mode |
Yes | gesture |
Follow valid contact and physical trackpad motion |
intensity |
Yes | 0–1 | Normalized strength |
fineness |
Yes | 0–1 | Influence pulse spacing/cadence |
reactivity |
Yes | 0–1 | Influence acceleration response |
No durationMs, pulseRateHz, sharpness or transitionMs is accepted. Without
valid contact and movement, no new pulses are produced. Remaining still does
not end the session. Intensity zero keeps a silent session. Renew every 500 ms;
it expires 2 seconds after the last accepted renewal.
Timed mode
{"mode":"timed","intensity":0.4,"fineness":0.5,"durationMs":300}
| Field | Required | Range | Meaning |
|---|---|---|---|
mode |
Yes | timed |
Run for a bounded duration |
intensity |
Yes | 0–1 | Normalized strength |
fineness |
Yes | 0–1 | Nominal cadence from 20 to 60 pulses/s |
durationMs |
Yes | 20–2000 ms | Total duration from the original start |
No movement is required. No reactivity, pulseRateHz, sharpness or
transitionMs is accepted. Heartbeats do not extend this mode.
Controlled mode
{"mode":"controlled","intensity":0.1,"pulseRateHz":6,"sharpness":0,"transitionMs":60}
| Field | Required | Range/default | Meaning |
|---|---|---|---|
mode |
Yes | controlled |
Follow application-defined targets |
intensity |
Yes | 0–1 | Normalized strength |
pulseRateHz |
Yes | 1–60 pulses/s | Requested cadence, independent of movement |
sharpness |
Yes | 0–1 | Soft to crisp; quantized to eight prepared profiles |
transitionMs |
No | 0–1000 ms; default 40 | Linear transition time for subsequent target changes |
No fineness, reactivity or durationMs is accepted. The initial values apply
directly. Later updates transition from the current interpolated values; zero
transition time applies the targets immediately. Intensity zero keeps the
session active. There is no fixed maximum duration while renewed, but a missed
renewal expires the session after 2 seconds.
sharpness changes the nominal profile from 6 ms at zero to 2 ms at one. It does
not expose a physical oscillator frequency. frequencyHz is unsupported.
POST /v1/resonance/update
Send a nonempty partial object with only fields applicable to the active mode:
{"intensity":0.65,"pulseRateHz":40,"sharpness":0.8}
Omitted fields keep their values; mode, null, unknown names and fields from
another mode are rejected. A rejected edit leaves the existing session intact.
The session must belong to the same client UUID.
| Active mode | Editable fields | Timing |
|---|---|---|
gesture |
intensity, fineness, reactivity |
Same motion session, without restarting contact monitoring |
timed |
intensity, fineness, durationMs |
Next emission uses current values; duration remains relative to original start |
controlled |
intensity, pulseRateHz, sharpness, transitionMs |
Same session, transitions from current values; works while paused |
An accepted update renews a gesture or controlled session. It does not restart
a timed effect. Shortening a timed duration below elapsed time ends it and
returns activeEffect: "none", resonance: null, remainingDurationMs: 0.
An absent or expired session returns 409 no_resonance without restarting.
POST /v1/resonance/pause and /v1/resonance/resume
Body: {}. Only for controlled mode owned by the same UUID. Response: current
resonance state, including paused.
These commands are idempotent and renew the lease. Pause suspends future
emissions but retains ownership and parameters. Transitions continue with wall
time while paused. Resume uses the current transition values and never replays
missed pulses. Continue heartbeats in pause. If no controlled session exists,
responds with 409 no_controlled_resonance.
POST /v1/sequences
{
"steps": [
{"style":"crisp","intensity":0.25,"delayMs":0},
{"style":"soft","intensity":0.2,"delayMs":180}
]
}
steps contains 1–16 objects. Each has required style, intensity and
delayMs. Style and intensity follow the impulse rules. Delay is before
each impact: 0–5000 ms for the first, 50–5000 ms for subsequent steps. The sum
of all delays must be at most 5000 ms. All numbers must be finite.
Response: {"ok":true,"activeEffect":"sequence"}. It confirms the scheduled
sequence, not the outcome of each hardware emission. The same UUID retains
ownership until completion, stop, replacement or loss of output. Missed impacts
are not queued for a later burst. There are no sequence update or heartbeat
extension commands.
POST /v1/heartbeat
Body: {}. Response: {"ok":true}. Renews only an existing gesture or
controlled session of the same UUID, including a controlled session in pause.
It never starts or revives a session. Send every 500 ms while the interaction is
wanted; a session expires 2 seconds after its latest accepted renewal.
An absent, expired, timed or sequence effect returns 409 no_gesture. The error
name is retained for protocol compatibility and also covers controlled mode.
POST /v1/stop
Body: {}. Response: {"ok":true}. Stops the current client’s effect, or
confirms that there is no active effect. Another UUID’s effect returns
409 busy. Further emissions are cancelled; an impulse already sent to the
hardware cannot be recalled.
Ownership and output limits
An active resonance or sequence belongs to its UUID. A different UUID receives
409 busy for POST commands until ownership ends. The owning UUID may replace
its effect with a new valid effect command. Impulses are one-shot and retain no
ownership afterward. The host application’s stop control can stop any session.
Starts, updates, impulses and sequences share a server limit of 60 effect commands/s. Use at most 20 live updates/s, coalescing intermediate values. Stop, heartbeat and pause/resume are not subject to that effect-command limiter; they still must obey ownership and other request rules.
A shared native budget allows up to 60 gentle emissions/s and enforces a 50 ms gap after a stronger impulse (20/s). Those are ceilings, not timing guarantees. The controlled profiles share the gentle amplitude ceiling across sharpness values. None of these values controls the physical actuator frequency.
The user’s intensityLimit multiplies requested intensity. For example, request
0.8 with a limit of 0.5 uses normalized intensity 0.4. The API cannot edit that
limit. Changing it in the host app stops the current effect. Disabling output,
changing/disconnecting the device, leaving API mode or stopping Feel My Mac also
ends the effect. Intensity zero alone does not end a resonance session.
Errors
Errors use an HTTP failure status and JSON, for example:
{"ok":false,"error":"output_unavailable","message":"Enable Haptic feedback and select an available trackpad"}
| HTTP | error |
Client action |
|---|---|---|
| 400 | invalid_client |
Supply a valid, stable client UUID for POST |
| 400 | invalid_parameters |
Check mode, field names, JSON types and limits |
| 400 | bad_request, incomplete_request |
Correct HTTP framing and request length |
| 403 | invalid_host, browser_origin_denied |
Use a desktop client on IPv4 loopback |
| 404 | not_found |
Use an exact documented endpoint |
| 405 | method_not_allowed |
Use the endpoint’s GET or POST method |
| 409 | busy |
Wait for the other owner to finish |
| 409 | no_resonance |
Start a new session only if still wanted |
| 409 | no_controlled_resonance |
Pause/resume needs an active controlled session |
| 409 | no_gesture |
No renewable session exists; do not continue heartbeats |
| 411 | length_required |
Include exact Content-Length on POST |
| 413 | request_too_large, body_too_large |
Reduce request size |
| 415 | json_required, encoding_not_supported |
Send uncompressed JSON |
| 417 | expect_not_supported |
Omit Expect |
| 429 | rate_limited |
Reduce effect-command rate; keep the newest target |
| 431 | headers_too_large |
Reduce headers below 8 KiB |
| 503 | mode_inactive, unavailable |
Restore the Local API service |
| 503 | output_unavailable |
Wait for valid licensed output and trackpad availability |
| 503 | gesture_unavailable |
Gesture contact monitoring could not start or update |
A stopped service normally causes a connection error, not a JSON response. Connections exceeding the simultaneous limit or the 3-second deadline are closed without a JSON error. Handle bounded network failures, stop your local interaction state and avoid unbounded retries. Do not retry one-shot impacts automatically after an ambiguous timeout. On sleep/wake or a lost session, recheck availability before starting an interaction that the user still wants.