API reference

Everything the MCP tools and the CLI do goes through this REST API — you can drive it directly with curl.

Basics

curl -s https://nomac.app/api/v1/me \
  -H "Authorization: Bearer $NOMAC_API_KEY"

Cloud Mac sessions

Mac time is prepaid credit, separate from Builder plans: $0.80 an hour, charged on each session's ready time rounded up to whole minutes, with a one-minute minimum. Credit is bought in $10 packs (12.5 hours), 1 to 10 per purchase, at Your Mac or through the checkout endpoint below. It never expires. There is no subscription and no automatic overage. One concurrent session per account; sessions may queue when capacity is busy.

GET/mac/credits

Returns credits (spendable, paid, promotional and reserved amounts as microdollar strings, spendable_seconds of Mac time at the current rate, spending status and pricing) and recent purchases. A returned checkout URL does not grant credit; only a verified payment does.

POST/mac/credits/checkout

JSON { "packs": 1-10 } ($10 each), with a stable Idempotency-Key. Returns action: open_checkout_url with a url for the human to open and pay, check_payment while a payment is being verified, or checkout_ended. Also returns the amount, credit hours and a message an agent can relay. A human pays; never enter payment details on their behalf. Reuse the key after a lost response; a new key is a new purchase.

GET/mac/capabilities

Read supported interfaces and profiles. GET /mac/sessions/:id also returns image: macOS, Xcode, Simulator runtimes and installed tools.

POST/mac/sessions

Stable Idempotency-Key required. Body: optional SSH public_key, profile: standard, and limits (max_duration_seconds, idle_timeout_seconds, spend_cap_microunits), and access?: { prompts?: "auto" | "ask", gatekeeper?: "off" | "on" }. The Mac is fully open to the agent by default (no administrator password prompts, its login password in $NOMAC_MAC_PASSWORD); the answer's warning says so. prompts: "ask" keeps macOS's usual prompts; permissions: { "com.example.App": ["camera", "microphone"] } (or "all") grants apps privacy permissions at start; gatekeeper asks for Gatekeeper's state, and the session's account.gatekeeper shows what the Mac has. The account's saved defaults apply first. Omit the SSH key for HTTPS-only access. Returns session; poll until ready. Inspect its billing mode, offer and deadlines. Defaults are 7,200 seconds duration (28,800 at most), 600 seconds idle and a $5 spend cap. With no credit it returns 402 insufficient_credit with next_action: buy_credit and a buy_url for the human.

GET/mac/sessions

List sessions to recover an allocation after losing a response.

GET / DELETE/mac/sessions/:id

Read session state or request shutdown. Queue/setup time is free. Ready time, including time between commands, counts after a one-minute minimum. Stop deletes the Mac and its files; poll until cleanup_confirmed.

POST/mac/sessions/:id/jobs

Body { argv: ["/usr/bin/uname", "-a"], cwd?: ".", env?: {}, timeout_seconds?: 600 }; stable Idempotency-Key required. Returns a durable job ID. For shell syntax use an explicit shell in argv. timeout_seconds defaults to 300 and is capped at 600; use SSH for longer work within session funding.

POST/mac/sessions/:id/jobs?wait=30

Waits up to wait seconds (0 to 50) for the command to finish. A finished command comes back with exit_code, and stdout and stderr as text (first 64 KiB each; stdout_more and stdout_next_cursor when there is more). A command still running comes back as a job to follow.

POST/mac/sessions/:id/files

Body { path: "app/config.json", content?: "text", content_base64?: "…", executable?: false }, up to 3 MiB. Relative paths are in the workspace; ~ is the home folder. Creates folders and replaces an existing file in one step; safe to repeat.

GET/mac/sessions/:id/files?path=build.log&max_bytes=1048576

Returns { path, size, modified_at, truncated, encoding: "utf8" | "base64", content | content_base64 }, up to 3 MiB.

POST/mac/sessions/:id/links

Body { direction: "upload", path, executable?, extract?: "zip" | "tar", expires_in_minutes? } or { direction: "download", path, expires_in_minutes?, wait_seconds? }. Moves one file of up to 2 GiB through storage instead of a request body. Returns url, a page anyone holding it can use without an account. An upload link also returns upload_url for curl -T and takes one file; a ready download link returns download_url (a folder comes as a zip) and keeps working after the Mac stops. Links last 60 minutes by default, 1440 at most.

POST/mac/sessions/:id/screenshot

Body { target?: "screen" | "simulator" }. Returns { image_base64, mime_type, width, height, path, front_app, screen_locked, dialog?, next_step? } (dialog names an open macOS dialog with its text and buttons): a JPEG of the whole desktop (1440x900; image pixels are screen coordinates) or of the booted iOS Simulator, scaled to at most 1568 pixels on its long side. simulator_not_booted (409) when no Simulator is running.

POST/mac/sessions/:id/input

Body { action: "type" | "key" | "click" | "move" | "scroll" | "password" | "unlock" | "size", text?, keys?, x?, y?, steps?, button?: "left" | "right", double?, screenshot? }. Real keyboard and mouse input, which macOS takes as a person's: it reaches the lock screen, the login window and password dialogs. password types the Mac's login password; unlock opens a locked screen and types nothing on an open one. Returns what was done and a screenshot taken right after (as the screenshot endpoint), unless screenshot: false. Not idempotent: a retry types again.

GET / PUT / DELETE/mac/defaults

The account's options for every new Mac: { prompts?, gatekeeper?, permissions? }, the same as a start's access. PUT replaces them, DELETE goes back to fully open. Returns defaults (as saved) and effective (what a new Mac gets). A start's own access wins field by field; its permissions add to the saved ones.

GET/mac/sessions/:id/password

Returns { session_id, user, password, variable }: the Mac's login password for this session, also in $NOMAC_MAC_PASSWORD on the Mac. Each read is audited. password_not_set (409) until the Mac is ready and its password is set. It is erased when the session ends.

POST / DELETE/mac/sessions/:id/recording

POST with { target?: "screen" | "simulator", max_seconds?: 300, path? } starts a video and returns at once; it stops by itself after max_seconds (1800 at most). One recording at a time: recording_in_progress (409) otherwise. DELETE stops it, saves the MP4 and returns recording (path, seconds, size) and a download link as above; safe to repeat.

GET / POST/mac/links/:token?wait=20

No API key: the link is the credential. GET returns its state: waiting_for_upload, placing, placed, copying, ready, failed or expired, and moves an upload that has arrived onto the Mac. POST does the same right after an upload, answering file_not_uploaded until it is done. GET /mac/links/:token/file redirects to a ready download.

GET/mac/sessions/:id/jobs/:job

Read the original job’s state and exit code. Never use a new key to retry an unknown command.

GET/mac/sessions/:id/jobs/:job/output?stream=stdout&cursor=0

Decode data_base64 and resume at next_cursor; read stderr too. Truncation does not kill a command. The retained prefix is at most 8 MiB combined. The final truncated response includes tail_base64, absolute tail_offset and total_bytes. Deduplicate overlap. HTTP 410 output_expired means older logs were removed, not that the job should run again. Retrieve files/logs before stopping.

POST/mac/sessions/:id/access

Returns a short-lived gateway token and verified SSH host key. The CLI handles pinned SSH over WSS/443. Direct gateway calls use the session token, not the account API key. Use CLI sync/pull or native SSH transfers for project files; the gateway blob store is limited to 8 MiB per blob.

POST / GET/report-issue

To report a Mac issue, send description, session_id and optional job_id. The server attaches only your account’s customer-safe session state. An optional email requests a resolution update. GET reads replies. Do not include signing keys or passwords.

Secrets, Apple signing and shipping

Secret values are stored encrypted and no endpoint returns them. Every Mac receives every-Mac secrets when it becomes ready: variables in every command's environment, files at their path, with their values masked in command output. The App Store Connect key, connected with POST /connections or at Apple, stays on NoMac's server: signing, TestFlight and notarization use it there and give a Mac only what Apple returns for one operation.

GET/secrets

Names, kinds, where each goes, sizes, delivery and recent activity. Never values.

PUT / DELETE/secrets/:name

PUT with { kind: "variable", value } or { kind: "file", value_base64, destination?: "~/.netrc" }, and delivery every_mac (default) or on_request. Up to 100 secrets, 32 KiB per variable, 1 MiB per file, 4 MiB in all. DELETE stops new Macs receiving it.

POST/secrets/requests

Body { name, kind, description, destination?, delivery? }. Returns a url where a signed-in person enters the value in the browser, valid for 24 hours, so it never passes through a chat.

POST/mac/sessions/:id/secrets

Body { names?: [] }. Installs every-Mac secrets, the on-request ones named and the signing files on a running Mac now; returns what is installed, by name and path.

GET/apple

Connected Apple accounts, the signing identities every Mac receives and the NoMac profiles, with any notice from the daily check. Never a key.

POST/apple/signing

Body { issuer_id? }. Sets up a distribution identity for the Apple account: an uploaded certificate, the one Apple already issued for NoMac's key, or a new one when the account has none.

POST / DELETE/apple/certificates

POST with { p12_base64, password } adds the certificate you already sign with (any export, Developer ID included). DELETE /apple/certificates/:id removes one from your Macs; nothing is revoked at Apple.

POST/mac/sessions/:id/signing

Body { path?, scheme?, container?, issuer_id? }. Reads the project on the Mac, makes the App Store profiles its app and extensions need, installs them, and returns files (an .xcconfig and export options), build_number (reserved for the app), commands for xcodebuild, Flutter and fastlane, and changes made at Apple. The project is not changed. needs_human (409) carries the steps and link when only a person can act.

POST/mac/sessions/:id/testflight

Body { path: "build/export/App.ipa", notes?, uses_non_exempt_encryption?, issuer_id?, request_key?, wait_seconds? }. Sends a signed .ipa on the Mac to TestFlight: NoMac creates the upload at Apple and the Mac sends only the file. Returns the send with state sending, processing, on_testflight or failed, its project_id and, once Apple has it, build_id for /publish. The same request_key follows the same send. encryption_answer_required (409) asks for the person's answer once per app; build_number_used (409) gives next_build_number.

GET/testflight/:id?wait_seconds=50

Follow a send, waiting up to 50 seconds for progress.

POST/mac/sessions/:id/notarize

Body { path: "build/App.app", issuer_id?, request_key?, wait_seconds? }, for a Developer ID-signed .app, .dmg, .pkg or .zip. The Mac sends it to Apple's notary service with credentials for this one submission, then staples the ticket. state: preparing, uploading, in_progress, stapling, then accepted (with stapled), invalid or rejected (with Apple's issues), or failed.

GET/notarizations/:id?wait_seconds=50

Follow a notarization, waiting up to 50 seconds for progress.

Account

GET/me

Your org, plan tier, how you authenticated, and project/connection counts.

GET/api-keys

List active keys (id, label, last used). The token itself is never shown again.

POST/api-keys

Body { "label"? }. Returns { id, label, token } — the token appears exactly once.

DELETE/api-keys/:id

Revoke a key.

Device login (what nomac login uses; both endpoints are unauthenticated — the code is the secret): POST /device/start returns { device_code, user_code, verification_uri, interval }; the human approves in the browser while the client polls POST /device/token with the device_code until { status: "approved", api_key } — the key is returned exactly once.

Apple connections, apps and the Builder

Apple connections and the App Store endpoints (metadata, screenshots, review, publish, TestFlight and the catalog) work for every account, on an app made by a TestFlight send or a Builder project. Snapshots and builds are the Builder, for existing subscribers only: its plans are closed to new subscriptions. None is required to rent a full Mac session.

POST/connections

Connect an App Store Connect API key. Body { p8, key_id, issuer_id } — validated live against Apple before anything is stored; the .p8 is encrypted at rest.

GET/connections

List connections with status, team name and signing-certificate expiry.

GET/connections/:id

One connection, health-checked.

DELETE/connections/:id

Remove a connection.

POST/connections/:id/webhooks

Body { asc_app_id, base_url? }. Registers NoMac as an ASC webhook receiver for build/version/feedback events, so state updates arrive without polling.

Projects & snapshots

POST/projects

Body { name, connection_id? } — if the org has exactly one Apple connection it is linked automatically.

GET/projects

List projects (id, name, bundle id, scheme, ASC app id).

GET/projects/:id

One project.

DELETE/projects/:id

Soft-delete a project.

POST/projects/:id/snapshots

The body is a gzipped tarball of the working tree (Content-Type: application/gzip) — this is nomac push. Secret-looking files are deny-listed at intake. Auto-detects .xcodeproj, scheme, bundle id and version, resolves the matching ASC app record, and runs source-level review lint. Returns { snapshot_id, commit, files, detected, asc_app_id, lint } plus a warning if no ASC app record exists for the bundle id yet.

GET/projects/:id/feedback

TestFlight return channel: crash reports and screenshot feedback from testers, each with device, OS and comment — structured for an agent to act on.

Builds

POST/builds

Body { project_id, workflow } where workflow is release (signed, uploaded to TestFlight — the default) or smoke (fast unsigned compile check). Builds the latest snapshot; build numbers increment automatically. Every attempt counts toward your monthly cap (Starter 50, Pro 300) — over cap returns 402.

GET/builds?project_id=…

Latest 50 builds, newest first.

GET/builds/:id

Build status. state walks queued → mirrored → dispatched → building → uploading → processing → ready (or failed); a full release run is typically ~3 minutes. Poll this — it also nudges the pipeline forward.

GET/builds/:id/logs

Per-step logs and, for failures, the relevant excerpt.

GET/builds/:id/failure

The structured failure an agent acts on: { stage, code, kind, error_lines, hints }. kind: "project" means fix the source; kind: "account" means a human must act (a deep link is included).

Review & App Store

POST/review-lint

Body { project_id }. The full review-readiness report: source findings from the latest push plus live store findings from ASC, each graded red/yellow/green with evidence and a fix hint. Some findings include ready-to-apply patches.

GET/metadata-schema

The deterministic contract for listing metadata: every field with its char limit, plus screenshot display types and their exact allowed pixel dimensions.

GET/projects/:id/metadata

Current App Store listing state, read live from ASC.

PUT/projects/:id/metadata

Write listing fields. Body { locale?, fields: { name, subtitle, privacy_policy_url, description, keywords, whats_new, support_url, marketing_url, promotional_text } } — all optional, validated against Apple's limits before any ASC write (422 with per-field errors otherwise). Also accepts primary_category, age_rating (human-attested answers), content_rights, copyright, review_contact (with optional demo account) and price ("FREE" in this convenience endpoint). Paid prices and schedules are available through the store catalog below.

POST/projects/:id/screenshots

Body { display_type, locale?, images: [{ filename, data }] } with base64 PNG data. Every image is validated locally for exact dimensions first, then the set for that display type is replaced all-or-nothing — a bad image never leaves a half-updated listing.

POST/publish

Body { project_id, build_id?, asc_submission_id?, resolve_rejection?, confirm?, force? }. Without confirm: staging that changes Apple drafts and attachments — preflights a ready build and the lint gate, creates/reuses the draft review submission, and returns Apple's own structured blocker list if anything is missing. With confirm: true: submits for review — agents must have their human's authorization. New versions use manual release; existing versions retain their Apple release policy. force: true overrides a red lint gate (still requires confirm).

GET/submissions/:id

Track a review submission's state after publish.

App Store Connect catalog

GET/store-capabilities?category=…&operation=…

Discover app-scoped Apple operations for listing/localization, paid pricing and availability, subscriptions and in-app purchases, offers, public customer reviews, phased releases, in-app events, custom product pages, files, analytics and webhooks. Filter with category or search; use offset/next_offset to paginate. Pass one operation for its exact Apple JSON:API body and query schema. The MCP equivalent is get_store_capabilities.

POST/projects/:id/store/read

Body { operation, scope?, query?, cursor?, resource_id? }. Executes only a documented Apple GET. Start with an app relationship, such as apps_subscriptionGroups_getToManyRelated, and scope:[]. Every returned child includes a reusable scope; copy it into subsequent reads and edits. Continue pages with next_cursor and the same query. Raw resource IDs are accepted only for global lookups such as territories. MCP: read_store.

POST/projects/:id/store/write

Body { operation, scope?, references?, body?, confirm? }. The nested body is the exact JSON:API document from discovery. Every existing app-resource ID in a relationship needs a verified scope in references; inline creates use local IDs such as ${price1}. Defaults to a preview. With confirm:true, performs the selected real change and records its target, outcome and audit. Use owner-authorized prices, messages and attestations. Replay interrupted requests with the same Idempotency-Key and body. MCP: update_store.

App submission, review resolution and beta distribution continue through publish, update_review and manage_testflight. Public customer-review responses can be sent through the catalog; Apple reviewer correspondence remains in App Store Connect.

POST/projects/:id/store/assets

Body { operation, scope?, references?, body, data_base64, confirm? }. Creates, uploads and commits a new screenshot, preview, purchase image, supplemental reviewer file or encryption document. Choose its asset createInstance operation and parent from discovery. Inline uploads support 2 MiB; the CLI MCP upload_store_asset also accepts file_path and streams larger files, subject to Apple limits. Keep the original file and request key to resume.

ready:true means Apple reports delivery COMPLETE; an uploaded file may still be processing or rejected. This adds a new file. Use upload_screenshots for managed replacement of an existing screenshot set. Supplemental reviewer files are separate from attachments to reviewer messages.

Review lifecycle and external TestFlight

GET/projects/:id/reviews?asc_submission_id=…

Without a filter, lists live Apple submissions, including those created outside NoMac. With an exact Apple ID, returns every item and app-version/build/contact/attachment details. Demo passwords are redacted. Apple reviewer message text is unavailable through the public API; the response includes the App Store Connect handoff.

POST/projects/:id/reviews

Body { asc_submission_id, action, item_id?, confirm?, force? }. Actions: resolve_item, remove_item, cancel, resubmit, release. Item actions and release require the exact item ID. Preview without confirmation; confirm:true performs the action. Fix the actual issue before resolving; resolve/remove every rejected item before resubmitting. Removal cannot be undone in the same submission. Release requires Pending Developer Release.

POST/projects/:id/reviews/reply

Body { asc_submission_id, reply }. Prepares the agent-authored reply and returns sent:false, state:requires_manual_send and the app link. The human sends it in App Store Connect. Apple does not expose a reviewer correspondence API; reviewer notes and public customer-review responses are different things.

GET/projects/:id/testflight?asc_build_id=…

Lists groups, test info and recent builds. An exact Apple build ID returns internal/external status, beta review and group assignments. Use group_id to list all testers in a group owned by this app. Internal readiness does not imply external approval.

POST/projects/:id/testflight

Actions: metadata, create_group, update_group, invite_tester, remove_tester, remove_build, distribute, notify, expire, encryption. Preview without confirm; confirm to apply. Metadata accepts locale, app_info (description, feedbackEmail, marketingUrl, privacyPolicyUrl), review_contact (Apple contact/demo fields), and build whats_new. Distribution requires an exact asc_build_id and explicit external group_ids. auto_notify defaults false; notify after approval or explicitly opt in. Encryption requires the owner's accurate uses_non_exempt_encryption answer. Expire affects all testers of that build.

Groups use group_id, name, public_link_enabled and public_link_limit (1–10,000). Invites require group_id and email with optional first_name/last_name; removal uses group_id and tester_id. Only the selected group membership changes.

For pending mutations, repeat the original body and Idempotency-Key; do not generate a new key or change the target. GET /projects/:id/operations recovers the saved request key and pending actions. A successful action is not an Apple approval decision. Review messages, appeals, account agreements, compliance documents and unsupported product edits still use App Store Connect.

Support

POST/report-issue

Body { description, ref_id?, email? } — pass a build/project/submission id and the full context is attached automatically. The optional email opts into a direct resolution update; otherwise, poll the GET endpoint. Deduped by error fingerprint and rate-limited to 10/day per org. The response returns a warning only when the effective contact is an Apple Private Relay address; ordinary addresses, including @icloud.com, do not trigger it.

GET/report-issue

Your tickets and their replies.

GET/health

Unauthenticated liveness check.

Also see the CLI reference (including the CI/CD guide), /install for agents, and llms.txt.