API reference
Everything the MCP tools and the CLI do goes through this REST API — you can drive it directly with curl.
Basics
- Base URL:
https://nomac.app/api/v1 - Auth:
Authorization: Bearer nmk_…— create keys in the dashboard, vianomac login, or withPOST /api-keys. - Requests and responses are JSON, except snapshot upload (gzipped tarball) — errors are always
{ "error": { "code", "message" } }with a meaningful HTTP status.402means a plan/quota cap;422means a precondition (no snapshot, no Apple connection, validation). - All POSTs accept an
Idempotency-Keyheader; duplicates replay the original response instead of repeating the action.
curl -s https://nomac.app/api/v1/me \ -H "Authorization: Bearer $NOMAC_API_KEY"
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
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 (v1: "FREE" only).
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, confirm?, force? }. Without confirm: a staged dry-run — 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 — irreversible, so agents must ask their human first. force: true overrides a red lint gate (still requires confirm).
GET/submissions/:id
Track a review submission's state after publish.
Support
POST/report-issue
Body { description, ref_id? } — pass a build/project/submission id and the full context is attached automatically. Deduped by error fingerprint, rate-limited to 10/day per org.
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.