Build API
Every Build action (10) and data model (2): endpoints, scopes, input, preconditions, events and examples.
Turn an idea into a real app: ERA composes it from its own building blocks, or your coding tools change your repository and ERA opens the pull request with a preview.
Module code build · 10 actions · 2 data models · 6 events. A key or token never does more here than the person behind it can; row-level security applies exactly as in the app. Scopes: build (everything), build:read (read only) or build:<capability>.
Resources
| Resource | Table | List and read | POST | PATCH | DELETE |
|---|---|---|---|---|---|
build_projects | build_project | /api/v1/build_projects | |||
build_runs | build_run | /api/v1/build_runs |
Lists take limit (up to 200), cursor, order (e.g. -created_at), fields and q; every other query parameter is a column filter: ?status=paid, ?total[gte]=100, ?id[in]=a,b, ?x=null.
Actions
| Action | Needs | What it does |
|---|---|---|
build.archive_project | build:delete | Archive a project |
build.cancel_run | build:write | Stop a build |
build.complete_run | build:handle | Finish a build |
build.create_project | build:write | Start a project |
build.fail_run | build:handle | Mark a build failed |
build.record_preview | build:handle | Record a preview |
build.record_progress | build:handle | Record build progress |
build.record_publish | build:handle | Record that an app went live |
build.request_run | build:write | Build or change something |
build.update_project | build:write | Change a project |
build.archive_project
Archive a project — Takes it off the Projects list. Its site, repository and pull requests are not touched.
Needs build:delete · removes or cannot be undone · asks a person to confirm when Era runs it
https://app.getera.io/api/v1/build_projects/{id}/actions/archive_project- Canonical address:
POST /api/v1/actions/build.archive_project
Input
| Field | Type | Required | Notes |
|---|---|---|---|
id | uuid | yes | Filled from the path {id} · Project · id of build_project |
Preconditions
The action is refused with 409 and one of these codes:
| Code | Refused when |
|---|---|
not_found | The project must exist in this workspace. |
run_open | Nothing may be building in it. |
What it writes: build_project (update) — → archived
curl -X POST https://app.getera.io/api/v1/build_projects/BUILD_PROJECT_ID/actions/archive_project \
-H "Authorization: Bearer $ERA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{}'{
"ok": true,
"result": "…",
"events": []
}import { createEra } from "@getera/sdk"
const era = createEra({ apiKey: process.env.ERA_API_KEY })
const res = await era.buildProjects.archiveProject("BUILD_PROJECT_ID")era build_projects archive_project BUILD_PROJECT_ID{
"name": "era__build__archive_project",
"arguments": {
"id": "BUILD_PROJECT_ID"
}
}Errors: 401 no or expired credential · 403 the credential does not hold build:delete · 404 the record or module is outside what it may reach · 422 invalid input (problems[]) · 409 not_found, run_open · 429 slow down
build.cancel_run
Stop a build — Stops it; the coding tool on the computer is stopped too. Nothing is committed.
Needs build:write · removes or cannot be undone
https://app.getera.io/api/v1/build_runs/{id}/actions/cancel_run- Canonical address:
POST /api/v1/actions/build.cancel_run
Input
| Field | Type | Required | Notes |
|---|---|---|---|
id | uuid | yes | Filled from the path {id} · Build · id of build_run |
Preconditions
The action is refused with 409 and one of these codes:
| Code | Refused when |
|---|---|
not_found | The build must exist. |
run_closed | Only an open build can be stopped. |
What it writes: build_run (update) — → cancelled; build_project (update) — Back to ready or draft
curl -X POST https://app.getera.io/api/v1/build_runs/BUILD_RUN_ID/actions/cancel_run \
-H "Authorization: Bearer $ERA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{}'{
"ok": true,
"result": "…",
"events": []
}import { createEra } from "@getera/sdk"
const era = createEra({ apiKey: process.env.ERA_API_KEY })
const res = await era.buildRuns.cancelRun("BUILD_RUN_ID")era build_runs cancel_run BUILD_RUN_ID{
"name": "era__build__cancel_run",
"arguments": {
"id": "BUILD_RUN_ID"
}
}Errors: 401 no or expired credential · 403 the credential does not hold build:write · 404 the record or module is outside what it may reach · 422 invalid input (problems[]) · 409 not_found, run_closed · 429 slow down
build.complete_run
Finish a build — Records what was built: the pull request and its files, or the composed app and where it is live. Machine-only.
Needs build:handle · changes data
403 machine_only).https://app.getera.io/api/v1/build_runs/{id}/actions/complete_run- Canonical address:
POST /api/v1/actions/build.complete_run
Input
| Field | Type | Required | Notes |
|---|---|---|---|
id | uuid | yes | Filled from the path {id} · Build · id of build_run |
summary | string | Summary · max 8000 | |
commitSha | string | Commit · max 64 | |
prNumber | number | Pull request · min 1 | |
prUrl | string | Pull request address · max 500 | |
previewUrl | string | Preview · max 500 | |
liveUrl | string | Live address · max 500 | |
filesChanged | number | Files changed · min 0 | |
changes | object / array | Changed files | |
usage | object / array | Usage | |
siteId | uuid | Site | |
blueprintCode | string | Blueprint · max 80 | |
plan | object / array | What was composed | |
awaitingPublish | boolean | Stopped at the draft | |
externalUrl | string | Expo build page · max 500 |
Preconditions
The action is refused with 409 and one of these codes:
| Code | Refused when |
|---|---|
not_found | The build must exist. |
run_closed | A build finishes once. |
Events it publishes: build.run_succeeded
What it writes: build_run (update) — → succeeded, with its result; build_project (update) — → ready, with its preview / live address
Errors: 401 no or expired credential · 403 the credential does not hold build:handle · 404 the record or module is outside what it may reach · 422 invalid input (problems[]) · 409 not_found, run_closed · 429 slow down
build.create_project
Start a project — An app ERA composes from its own building blocks, or a code repository ERA works on with your coding tools.
Needs build:write · changes data
https://app.getera.io/api/v1/build_projects/actions/create_project- Canonical address:
POST /api/v1/actions/build.create_project
Input
| Field | Type | Required | Notes |
|---|---|---|---|
kind | string (enum) | yes | Kind · Allowed values: app, code |
name | string | yes | Name · max 160 |
description | string | What it is · max 4000 | |
credentialId | uuid | GitHub / GitLab connection · id of credential | |
repoProvider | string (enum) | Provider · Allowed values: github, gitlab | |
repoFullName | string | Repository · owner/name · max 200 | |
repoUrl | string | Repository address · max 400 | |
defaultBranch | string | Main branch · max 200 | |
harness | string | Coding tool · claude = Claude Code · codex = Codex · gemini = Antigravity · grok = Grok Build · copilot = Copilot CLI · cursor = Cursor CLI · kimi = Kimi Code CLI · vibe = Mistral Vibe · opencode = opencode · amp = Amp CLI · kiro = Kiro CLI · cline = Cline CLI · auggie = Auggie CLI · max 32 |
Preconditions
The action is refused with 409 and one of these codes:
| Code | Refused when |
|---|---|
empty_name | A project needs a name. |
repo_incomplete | A repository needs its connection, provider and full name together. |
credential_not_found | The repository connection must be a GitHub or GitLab connection of this workspace. |
connection_not_usable | The connection must be the person’s own, the workspace’s, or shared with them. |
harness_not_supported | The coding tool must be one the Harness Manager runs. |
Events it publishes: build.project_created
What it writes: build_project (insert) — Writes the project
curl -X POST https://app.getera.io/api/v1/build_projects/actions/create_project \
-H "Authorization: Bearer $ERA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"kind":"app","name":"Name"}'{
"ok": true,
"result": "…",
"events": [
"build.project_created"
]
}import { createEra } from "@getera/sdk"
const era = createEra({ apiKey: process.env.ERA_API_KEY })
const res = await era.buildProjects.createProject({
kind: "app",
name: "Name"
})era build_projects create_project \
--kind "app" \
--name "Name"{
"name": "era__build__create_project",
"arguments": {
"kind": "app",
"name": "Name"
}
}Errors: 401 no or expired credential · 403 the credential does not hold build:write · 404 the record or module is outside what it may reach · 422 invalid input (problems[]) · 409 empty_name, repo_incomplete, credential_not_found, connection_not_usable, harness_not_supported · 429 slow down
build.fail_run
Mark a build failed — Records why it stopped and at which step. Machine-only.
Needs build:handle · changes data
403 machine_only).https://app.getera.io/api/v1/build_runs/{id}/actions/fail_run- Canonical address:
POST /api/v1/actions/build.fail_run
Input
| Field | Type | Required | Notes |
|---|---|---|---|
id | uuid | yes | Filled from the path {id} · Build · id of build_run |
error | string | Why · max 2000 | |
step | string | Step · max 40 |
Preconditions
The action is refused with 409 and one of these codes:
| Code | Refused when |
|---|---|
not_found | The build must exist. |
run_closed | A build finishes once. |
Events it publishes: build.run_failed
What it writes: build_run (update) — → failed; build_project (update) — → failed, unless an earlier build left it ready
Errors: 401 no or expired credential · 403 the credential does not hold build:handle · 404 the record or module is outside what it may reach · 422 invalid input (problems[]) · 409 not_found, run_closed · 429 slow down
build.record_preview
Record a preview — The pull request’s preview deployment appeared (Vercel, Netlify, Cloudflare Pages…). Machine-only.
Needs build:handle · changes data
403 machine_only).https://app.getera.io/api/v1/build_runs/{id}/actions/record_preview- Canonical address:
POST /api/v1/actions/build.record_preview
Input
| Field | Type | Required | Notes |
|---|---|---|---|
id | uuid | yes | Filled from the path {id} · Build · id of build_run |
previewUrl | string | yes | Preview · max 500 |
Preconditions
The action is refused with 409 and one of these codes:
| Code | Refused when |
|---|---|
not_found | Only a finished build gets a preview. |
bad_url | A preview is a web address. |
Events it publishes: build.preview_ready
What it writes: build_run (update) — Sets the preview; build_project (update) — Sets the project’s latest preview
Errors: 401 no or expired credential · 403 the credential does not hold build:handle · 404 the record or module is outside what it may reach · 422 invalid input (problems[]) · 409 not_found, bad_url · 429 slow down
build.record_progress
Record build progress — Moves a build to its next step (getting the repo ready, writing, opening the pull request). Machine-only.
Needs build:handle · changes data
403 machine_only).https://app.getera.io/api/v1/build_runs/{id}/actions/record_progress- Canonical address:
POST /api/v1/actions/build.record_progress
Input
| Field | Type | Required | Notes |
|---|---|---|---|
id | uuid | yes | Filled from the path {id} · Build · id of build_run |
status | string (enum) | Status · Allowed values: waiting, running | |
step | string | Step · max 40 | |
stepStatus | string (enum) | Step status · Allowed values: doing, done, failed, skipped | |
detail | string | Detail · max 300 | |
harness | string | Coding tool · max 20 | |
deviceId | string | Computer · max 64 | |
runnerJobId | string | Runner job · max 64 | |
branch | string | Branch · max 200 | |
baseBranch | string | Base branch · max 200 | |
baseSha | string | Base commit · max 64 | |
externalId | string | Expo build · max 120 | |
externalUrl | string | Expo build page · max 500 |
Preconditions
The action is refused with 409 and one of these codes:
| Code | Refused when |
|---|---|
not_found | The build must exist. |
run_closed | Only an open build moves. |
What it writes: build_run (update) — Status, step and where it runs
Errors: 401 no or expired credential · 403 the credential does not hold build:handle · 404 the record or module is outside what it may reach · 422 invalid input (problems[]) · 409 not_found, run_closed · 429 slow down
build.record_publish
Record that an app went live — The person approved the draft and its site was published (site.publish, as them). Machine-only: it records the live address.
Needs build:handle · reaches outside (sends, publishes or charges)
403 machine_only).https://app.getera.io/api/v1/build_projects/{id}/actions/record_publish- Canonical address:
POST /api/v1/actions/build.record_publish
Input
| Field | Type | Required | Notes |
|---|---|---|---|
id | uuid | yes | Filled from the path {id} · Project · id of build_project |
liveUrl | string | yes | Live address · max 500 |
Preconditions
The action is refused with 409 and one of these codes:
| Code | Refused when |
|---|---|
bad_url | A live address is a web address. |
not_found | The project must exist. |
not_draft | Only an app waiting at its draft is published. |
Events it publishes: build.app_published
What it writes: build_project (update) — → live, with its address; build_run (update) — The last app run’s "live" step done
Errors: 401 no or expired credential · 403 the credential does not hold build:handle · 404 the record or module is outside what it may reach · 422 invalid input (problems[]) · 409 bad_url, not_found, not_draft · 429 slow down
build.request_run
Build or change something — For an app project: ERA composes the app from its building blocks (modules, workflows, Eras, a site) and shows it as a draft to publish — or puts it live when you say "publish it". With a linked repository, your coding tool (Claude Code, Codex, Antigravity, Copilot, Cursor … any the Harness Manager runs) changes the code on your computer and ERA opens a pull request with a preview; "mobile" asks Expo for an iPhone / Android build.
Needs build:write · changes data
https://app.getera.io/api/v1/build_projects/{id}/actions/request_run- Canonical address:
POST /api/v1/actions/build.request_run
Input
| Field | Type | Required | Notes |
|---|---|---|---|
projectId | uuid | yes | Filled from the path {id} · Project · id of build_project |
prompt | string | yes | What to build or change · max 8000 |
mode | string (enum) | Kind of run · Allowed values: compose, code, mobile | |
publish | boolean | Publish when done | |
harness | string | Coding tool · claude = Claude Code · codex = Codex · gemini = Antigravity · grok = Grok Build · copilot = Copilot CLI · cursor = Cursor CLI · kimi = Kimi Code CLI · vibe = Mistral Vibe · opencode = opencode · amp = Amp CLI · kiro = Kiro CLI · cline = Cline CLI · auggie = Auggie CLI · max 32 | |
deviceId | string | Computer · max 64 | |
via | string (enum) | Asked from · Allowed values: web, era, work, phone, workflow, desktop | |
workStepId | uuid | Plan step |
Preconditions
The action is refused with 409 and one of these codes:
| Code | Refused when |
|---|---|
empty_prompt | There must be something to build. |
not_found | The project must exist and not be archived. |
no_repo | Changing code or building for phones needs the repository connected. |
no_mobile | A phone build needs the Expo project set. |
harness_not_supported | The coding tool must be one the Harness Manager runs. |
connection_not_usable | The repository’s connection must be usable by the person asking. |
run_open | One build at a time per project. |
Events it publishes: build.run_requested
What it writes: build_run (insert) — Queues the run with its steps; build_project (update) — → building
curl -X POST https://app.getera.io/api/v1/build_projects/BUILD_PROJECT_ID/actions/request_run \
-H "Authorization: Bearer $ERA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"prompt":"What to build or change"}'{
"ok": true,
"result": "…",
"events": [
"build.run_requested"
]
}import { createEra } from "@getera/sdk"
const era = createEra({ apiKey: process.env.ERA_API_KEY })
const res = await era.buildProjects.requestRun("BUILD_PROJECT_ID", {
prompt: "What to build or change"
})era build_projects request_run BUILD_PROJECT_ID \
--prompt "What to build or change"{
"name": "era__build__request_run",
"arguments": {
"projectId": "BUILD_PROJECT_ID",
"prompt": "What to build or change"
}
}Errors: 401 no or expired credential · 403 the credential does not hold build:write · 404 the record or module is outside what it may reach · 422 invalid input (problems[]) · 409 empty_prompt, not_found, no_repo, no_mobile, harness_not_supported, connection_not_usable, run_open · 429 slow down
build.update_project
Change a project — Rename it, describe it, connect its repository (an app’s custom code too), pick the coding tool and computer, or set the Expo project for phone builds.
Needs build:write · changes data
https://app.getera.io/api/v1/build_projects/{id}/actions/update_project- Canonical address:
POST /api/v1/actions/build.update_project
Input
| Field | Type | Required | Notes |
|---|---|---|---|
id | uuid | yes | Filled from the path {id} · Project · id of build_project |
name | string | Name · max 160 | |
description | string | What it is · max 4000 | |
credentialId | uuid | GitHub / GitLab connection · id of credential | |
repoProvider | string (enum) | Provider · Allowed values: github, gitlab | |
repoFullName | string | Repository · owner/name · max 200 | |
repoUrl | string | Repository address · max 400 | |
defaultBranch | string | Main branch · max 200 | |
harness | string | Coding tool · claude = Claude Code · codex = Codex · gemini = Antigravity · grok = Grok Build · copilot = Copilot CLI · cursor = Cursor CLI · kimi = Kimi Code CLI · vibe = Mistral Vibe · opencode = opencode · amp = Amp CLI · kiro = Kiro CLI · cline = Cline CLI · auggie = Auggie CLI · max 32 | |
deviceId | string | Computer · A paired computer (Era Desktop or the cloud computer); empty = the first one that is on. · max 64 | |
expoProjectId | string | Expo project (phone builds) · expo.dev → your project → Project ID. Empty = no phone builds. · max 64 | |
platforms | object / array | Phones · ["ios","android"] |
Preconditions
The action is refused with 409 and one of these codes:
| Code | Refused when |
|---|---|
not_found | The project must exist in this workspace. |
empty_name | A project needs a name. |
repo_incomplete | A repository needs its connection, provider and full name together. |
credential_not_found | The repository connection must be a GitHub or GitLab connection of this workspace. |
connection_not_usable | The connection must be the person’s own, the workspace’s, or shared with them. |
harness_not_supported | The coding tool must be one the Harness Manager runs. |
bad_mobile | A phone build names its Expo project (a uuid) and iPhone and/or Android. |
What it writes: build_project (update) — Changes the project
curl -X POST https://app.getera.io/api/v1/build_projects/BUILD_PROJECT_ID/actions/update_project \
-H "Authorization: Bearer $ERA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"name":"Name"}'{
"ok": true,
"result": "…",
"events": []
}import { createEra } from "@getera/sdk"
const era = createEra({ apiKey: process.env.ERA_API_KEY })
const res = await era.buildProjects.updateProject("BUILD_PROJECT_ID", {
name: "Name"
})era build_projects update_project BUILD_PROJECT_ID \
--name "Name"{
"name": "era__build__update_project",
"arguments": {
"id": "BUILD_PROJECT_ID",
"name": "Name"
}
}Errors: 401 no or expired credential · 403 the credential does not hold build:write · 404 the record or module is outside what it may reach · 422 invalid input (problems[]) · 409 not_found, empty_name, repo_incomplete, credential_not_found, connection_not_usable, harness_not_supported, bad_mobile · 429 slow down
Data models
BuildProject
Table build_project · Resource build_projects
GET /api/v1/build_projectsGET /api/v1/build_projects/{id}
| Column | Type | Nullable | Allowed values | References |
|---|---|---|---|---|
id | string (uuid) · uuid | |||
organization_id | string (uuid) · uuid | organization | ||
workspace_id | string (uuid) · uuid | workspace | ||
created_by | string (uuid) · uuid | yes | app_user | |
kind | string · text | app, code | ||
name | string · text | |||
description | string · text | yes | ||
status | string · text | draft, building, ready, failed, archived | ||
repo_provider | string · text | yes | ||
repo_full_name | string · text | yes | ||
repo_url | string · text | yes | ||
default_branch | string · text | yes | ||
harness | string · text | yes | ||
device_id | string · text | yes | ||
blueprint_code | string · text | yes | ||
plan | object · jsonb | |||
site_id | string (uuid) · uuid | yes | ||
preview_url | string · text | yes | ||
live_url | string · text | yes | ||
last_run_id | string (uuid) · uuid | yes | ||
created_at | string (date-time) · timestamptz | |||
updated_at | string (date-time) · timestamptz | |||
site_state | string · text | yes | ||
mobile | object · jsonb |
Primary key: id
Checks:
CHECK (((harness IS NULL) OR (harness ~ '^[a-z][a-z0-9_]{1,31}$'::text)))CHECK ((length(btrim(name)) > 0))CHECK (((repo_provider IS NULL) OR (repo_provider = ANY (ARRAY['github'::text, 'gitlab'::text]))))CHECK (((site_state IS NULL) OR (site_state = ANY (ARRAY['draft'::text, 'live'::text]))))
BuildRun
Table build_run · Resource build_runs
GET /api/v1/build_runsGET /api/v1/build_runs/{id}
| Column | Type | Nullable | Allowed values | References |
|---|---|---|---|---|
id | string (uuid) · uuid | |||
organization_id | string (uuid) · uuid | organization | ||
workspace_id | string (uuid) · uuid | workspace | ||
project_id | string (uuid) · uuid | build_project | ||
created_by | string (uuid) · uuid | yes | app_user | |
kind | string · text | compose, code, mobile | ||
prompt | string · text | |||
status | string · text | queued, waiting, running, succeeded, failed, cancelled | ||
steps | object · jsonb | |||
harness | string · text | yes | ||
device_id | string · text | yes | ||
runner_job_id | string · text | yes | ||
branch | string · text | yes | ||
base_branch | string · text | yes | ||
base_sha | string · text | yes | ||
commit_sha | string · text | yes | ||
pr_number | integer · int4 | yes | ||
pr_url | string · text | yes | ||
preview_url | string · text | yes | ||
live_url | string · text | yes | ||
files_changed | integer · int4 | |||
changes | object · jsonb | |||
summary | string · text | yes | ||
error | string · text | yes | ||
usage | object · jsonb | yes | ||
requested_via | string · text | web, era, work, phone, workflow, desktop | ||
work_step_id | string (uuid) · uuid | yes | ||
started_at | string (date-time) · timestamptz | yes | ||
finished_at | string (date-time) · timestamptz | yes | ||
created_at | string (date-time) · timestamptz | |||
updated_at | string (date-time) · timestamptz | |||
publish | boolean · bool | yes | ||
external_id | string · text | yes | ||
external_url | string · text | yes |
Primary key: id
Events
Workflows and webhooks react to these events.
| Event | What happens | Payload |
|---|---|---|
build.app_published | An app’s draft was approved and is live. | projectId, liveUrl, recipientUserId |
build.preview_ready | A pull request’s preview deployment is ready to look at. | id, projectId, previewUrl, recipientUserId |
build.project_created | A project was started (an app or a code repository). | id, kind, name |
build.run_failed | A build did not finish. | id, projectId, error, recipientUserId |
build.run_requested | Somebody (or Era) asked for something to be built. | id, projectId, kind, harness |
build.run_succeeded | A build finished: a pull request was opened, the app is ready (live, or a draft waiting for Publish), or a phone build is ready. | id, projectId, kind, prUrl, previewUrl, liveUrl, awaitingPublish, recipientUserId |