Skip to content

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, or null if 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, pulseRateHz and sharpness at 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.