Macapad
Control your Mac from your phone, a tablet, or a screen on the wall.
-
Install it
Paste this into Terminal on your Mac.
curl -fsSL https://raw.githubusercontent.com/AD-Archer/Macapad/main/scripts/install.sh | bashTakes a few minutes the first time. Run it again any time to update.
What does it do?
It downloads the latest release, builds it on your Mac, and puts it in Applications. Because your Mac built it, macOS opens it with no warnings and keeps its permissions when you update.
It needs Apple’s Command Line Tools. If you don’t have them, their installer opens. Finish it, then run the command again.
Needs an Apple Silicon Mac on macOS 14 or later. Rather use Homebrew or a download?
-
Let it press keys
A smiling face appears in the menu bar. Then open System Settings → Privacy & Security → Accessibility and turn on Macapad.
-
Connect a device
Pick a Mac. You’ll confirm on the Mac.
Apps for it
Other ways to install
The one command is easiest. These work too, with one extra step.
Homebrew
brew tap ad-archer/tap
brew trust ad-archer/tap
brew install --cask macapad
xattr -dr com.apple.quarantine /Applications/Macapad.appTo update: brew upgrade --cask macapad, then the xattr line again.
Why brew trust?
Homebrew 6 and later only load taps you trust. To trust only Macapad instead of the whole tap:
brew trust --cask ad-archer/tap/macapad
Download
- Download
Macapad.zipfrom Releases. - Unzip it and drag
Macapad.appto Applications. - Run this in Terminal:
xattr -dr com.apple.quarantine /Applications/Macapad.appAfter every update, open System Settings → Privacy & Security → Accessibility, remove Macapad, and add it again.
macOS sees each new download as a different app. The one command doesn’t have this problem.
Why the xattr line?
Macapad isn’t signed with an Apple Developer ID. macOS marks downloads as quarantined and won’t open an unsigned one, often saying it “is damaged”. This line removes that mark. Only do it for apps you trust.
Build it yourself
You need the Command Line Tools (xcode-select --install) and just (brew install just).
git clone https://github.com/AD-Archer/Macapad.git
cd Macapad
just cert # once
just installWhat does just cert do?
It adds a self-signed “Macapad Dev” certificate to your keychain. Every build signs with it, so macOS keeps the Accessibility permission across rebuilds.
Run just to see the other tasks. just dev runs from source, and just tester serves this page with discovery.
Try code that isn’t released yet
The one command can build any branch or tag:
curl -fsSL https://raw.githubusercontent.com/AD-Archer/Macapad/main/scripts/install.sh | MACAPAD_REF=main bashPairing
How a device gets its own token. Use a QR code if it has a camera, or a 4-digit code if not.
With a QR code
No prompt on the Mac: seeing its screen is the proof.
- On the Mac, choose Pair with QR Code… in the menu bar.
- Scan it. It holds a link:
macapad://pair?v=1&id=<mac id>&name=<mac name>&key=<one-time key> &url=http://192.168.1.20:7317 &url=http://100.101.102.103:7317 - Try each
urlin order and use the first that answersGET /api/health. - Trade the key for a token:
POST <url>/api/pair/qr {"key": "<one-time key>", "name": "Kitchen iPad", "app": "archersdesk"} → {"status": "approved", "token": "…", "device": {…}, "mac": {"id": "…", "name": "…"}}
410. The token is only sent once, so save it.More about the link
There’s one url per address the Mac has, best first: local network, then Tailscale. Values are percent-encoded (except : and /), so any URL parser reads them back.
To pair this browser from one: Scan a QR code (needs HTTPS or localhost for the camera), or paste the link from Copy Pairing Link into Type an address.
With a code
The Mac asks before allowing anything.
POST /api/pairwith{"name": "Kitchen iPad", "app": "archersdesk"}. You get{"id", "code", "expiresIn", "mac"}.- Show the 4-digit
code. The Mac shows the same code with Allow and Don’t Allow. - Poll
GET /api/pair/<id>?wait=25. Each call waits up to 25 seconds, then returnspending,approved(withtokenanddevice),deniedorexpired.
Finding Macs
- Same Wi-Fi: Bonjour,
_macapad._tcp. The TXT record hasid,nameandv. - Tailscale: no Bonjour there. List devices with
tailscale status --jsonand call/api/healthon each. A Mac you can pair with answers with amacfield.
The same Mac found both ways has the same mac.id.
Steps
Everything the Mac can do is a step, like {"type": "media", "key": "next"}. A request runs a list of them in order and stops at the first that fails.
Pair with a Mac to try these.
Key names for keystroke
Any modifiers (cmd shift opt/alt ctrl fn) plus one key:
a–z,0–9, punctuation,f1–f20leftrightupdownspacereturntabescdeleteforwarddeletehomeendpageuppagedown
Keys are positions on a US keyboard.
media steps are real media-key presses, so they control whatever is playing.
Run your own
Actions
A named list of steps. Run one with {"type": "action", "id": "reload"}.
{"id": "reload", "name": "Reload", "icon": "refresh", "category": "browser",
"steps": [{"type": "keystroke", "keys": "cmd+r"}], "custom": false}
- Built-ins cover media, the system, windows, browsers and editing.
- Save your own with
POST /api/actions. They’re kept in~/.macapad/actions.json. - Ids use lowercase letters, numbers and dashes.
- Actions can run other actions, up to 4 deep.
iconis a Material Symbols name, a hint for frontends.
On your Mac
Pair with a Mac to see its actions and tap one to run it.
HTTP API
Send your token as Authorization: Bearer <token>. Health and pairing don’t need one.
Pair with a Mac to try the GET routes.
Errors are a 4xx with {"error": {"message": "…"}}.
From a script
curl -X POST http://localhost:7317/api/run \
-H "Authorization: Bearer $(cat ~/.macapad/token)" \
-H "X-Macapad-Client: My script" \
-H "Content-Type: application/json" \
-d '{"steps":[{"type":"media","key":"playPause"}]}'WebSocket
For a frontend that stays open. Quicker than a request per press.
- Connect to
/api/ws?token=<token>. Add&client=<name>if you name your client. (Browsers can’t set headers here.) - Send
{"type": "run", "ref": "<anything>", "steps": [...]}. You get{"type": "result", "ref", "ok", "error", "off"?}. - Send
{"type": "ping"}about every 25 seconds so proxies keep it open. You get{"type": "pong"}.
off means nothing ran because you’re paused or outside your schedule. If the device is removed in Settings, the connection closes the next time it sends anything.const ws = new WebSocket(`${base.replace(/^http/, "ws")}/api/ws?token=${token}`);
ws.onmessage = (e) => {
const msg = JSON.parse(e.data);
if (msg.type === "result" && !msg.ok) alert(msg.error);
};
ws.onopen = () => {
ws.send(JSON.stringify({ type: "run", ref: "skip", steps: [{ type: "media", key: "next" }] }));
};
Pair with a Mac to try it.
Live input
For controllers and trackpads: hold keys down and move the pointer in real time.
Send your whole state each time, not presses and releases, so a lost message can’t leave a key stuck:
{"n": 42, "keys": ["up", "z"], "buttons": ["left"], "dx": 3, "dy": -2, "sx": 0, "sy": 0}
ngoes up by one each time; older ones are dropped.keysis every key held right now:keystrokekey names, the modifierscmdshiftoptctrlfn, and media keys likevolumeUp(pressed once). Held keys repeat like a real keyboard.buttons: mouse buttons held,leftrightmiddle.dx,dymove the pointer andsx,syscroll (pixels; positivesyscrolls down), counted since the last message.
Send a message as soon as anything changes, at least every 100 ms while anything is held, and once a second otherwise. The Mac lets go of everything you hold when it hears nothing for half a second. When several clients hold keys at once, a key stays down while any of them holds it.
Over the WebSocket
Send {"type": "input", ...state}. While you’re paused you get {"type": "error", "reason": "off", "message"}.
Over UDP
Lower latency, for consoles. POST /api/input/session returns {session, udpPort, expiresAfter}. Send each state as a JSON datagram, with "s": session, to the Mac’s udpPort. Add "ack": true about once a second and the Mac answers {"type": "ok"}. A bad or expired session gets {"type": "error", "reason": "session"}: ask for a new one.
Try it: a trackpad
Pair with a Mac to try it.
Networks & tokens
Where to reach the Mac from, and which token to use.
Addresses
| From | Base URL |
|---|---|
| Same Wi-Fi | http://<mac-name>.local:7317 or http://<lan-ip>:7317 |
| Your tailnet | http://<tailscale-ip>:7317 |
| Tailnet over HTTPS | https://<mac>.<tailnet>.ts.net after tailscale serve --bg 7317 |
https:// must use the HTTPS address. Browsers block HTTPS pages from calling http://.Macapad’s Settings choose which networks may connect and which may pair.
- Programs on the Mac itself are always allowed.
- Anything from a network that’s turned off gets
403. - Pairing is never allowed from the internet.
Tokens
- Device tokens come from pairing. Each device has its own. Removing one in Settings cuts off only that device.
- The owner token is in
~/.macapad/token(menu bar: Copy API Token). It has full access. Use it for scripts and hand-set-up frontends.
Naming your client
With the owner token, say who’s calling, or you get 400:
X-Macapad-Client: Office display
On the WebSocket, use ?client=. Percent-encode names that aren’t plain ASCII.
Each name shows up in Settings with its own schedule and pause, so one server can drive several screens with different hours.
Paired devices and client ids
Paired devices already have a name, so naming is optional. One that sends it gets a named client under the device, which follows the device’s schedule unless you pick another.
Add X-Macapad-Client-Id (or ?clientId=), a stable id, to keep a client’s settings when it’s renamed.