Skip to content

Push and publish a version

Markdown

Once the files are written, a version goes live in a few calls: push it, fix what the validation found, try one of its actions, then put it in production.

The examples use $TOKEN, $API_KEY and $APP_ID as Build an application defines them.

Reserve a deposit, then PUT the archive to the URL it returns. The URL carries its own authorisation and works once, so the script that uploads holds no key of yours.

Terminal window
VERSION=$(curl -s -X POST "https://api.eu.apigami.com/v1/apps/$APP_ID/versions" \
-H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-d '{"mode":"archive"}')
URL=$(printf '%s' "$VERSION" | jq -r '.upload.url')
VERSION_ID=$(printf '%s' "$VERSION" | jq -r '.version_id')
cd project && zip -qr - . | curl -s -X PUT "https://api.eu.apigami.com$URL" \
-H 'Content-Type: application/zip' --data-binary @-
curl -s "https://api.eu.apigami.com/v1/apps/$APP_ID/versions/$VERSION_ID" -H "Authorization: Bearer $TOKEN"

Reserving receives nothing: the version exists once the PUT lands.

Send the changes instead of the files. POST /v1/apps/{app}/versions with base_version_id and a list of changes builds a new version from an existing one.

Request body
{
"base_version_id": "VERSION_ID",
"changes": [
{ "op": "write", "path": "actions/basket.ts", "content": "export const readBasket = …" },
{ "op": "replace", "path": "main.ts", "old_text": "'POST /search'", "new_text": "'POST /search'" },
{ "op": "delete", "path": "actions/old.ts" }
]
}

A replace whose old_text appears zero times or more than once is refused, and nothing is written: an edit that lands in the wrong place is worse than an edit that does not land.

Only bytes your environment can actually read can be sent. An image you can see in a conversation is not a file you can upload — ask for its path, or have the file written as text.

A logo, or a corrected manifest, without resending the rest: reserve a deposit with mode: "file", a base_version_id and the path the bytes land on, then PUT the bytes.

Terminal window
curl -s -X POST "https://api.eu.apigami.com/v1/apps/$APP_ID/versions" \
-H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-d '{"mode":"file","base_version_id":"'"$VERSION_ID"'","path":"public/logo.svg"}'

Receiving a version and being a valid project are two different results. A version whose manifest is missing or wrong is kept, and its diagnostics name the file, the field and the fix:

{
"version_id": "…",
"state": "ready",
"validation": {
"status": "invalid",
"errors": 1,
"diagnostics": [
{
"code": "missing_operation_id",
"file": "manifest.json",
"pointer": "/paths/~1basket/post",
"message": "Give this operation an `operationId`: it is the name of the action."
}
]
}
}

Fix the file it names with one replace, on the same base_version_id. Nothing unchanged is sent again, and you never have to read the whole project back to correct one line.

Terminal window
# Run an action of a precise version — this is how you try one. It takes an API key of the application.
curl -s -X POST "https://api.eu.apigami.com/v1/apps/$APP_ID/actions/searchProducts" \
-H "Authorization: Bearer $API_KEY" -H 'Content-Type: application/json' \
-H "X-Apigami-Version: $VERSION_ID" \
-d '{"query":"coffee"}'
# Deploy it, and check its health.
curl -s -X PATCH "https://api.eu.apigami.com/v1/apps/$APP_ID/versions/$VERSION_ID" \
-H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' -d '{"deployed":true}'
# Put it in production. The same call on an earlier version is the rollback.
curl -s -X PATCH "https://api.eu.apigami.com/v1/apps/$APP_ID/versions/$VERSION_ID" \
-H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' -d '{"production":true}'

Work in bounded chunks. Apigami waits up to 900 seconds for an action, fifteen minutes, and less when another run calls it: a child never outlives its parent. Within that time, each request has 300 seconds of computing time, and an instance 128 MB of memory shared by the requests it serves at once (Limits); past either, the run fails with a sentence that names the limit. Its caller waits thirty seconds, then gets 202 { run_id, state: "running" } while the run goes on; GET /v1/runs/{run_id} reads its outcome with a member’s credential, never with a key or a page’s token. So have each run do a short chunk, save its result and its progress in the database, and let a trigger or the next call carry on from there.

There is no try route: trying is running, with X-Apigami-Version naming the version. A member of the owning group may name one; everyone else always reaches the version in production.

A version that has no deployment yet runs in a sandbox, a trial, which is what lets you try code before you deploy it. The answer says which of the two served it, and only a run served by the version’s own deployment that answered below 500 counts towards publishing.

Publication is refused in three cases, and each one says what to do: a version none of whose actions has answered below 500 on its own deployment, a version whose last GET /healthz did not answer 200, and a version one of whose pages answered an error (page_failed). After GET /healthz, a deployment asks each page with a relative page_url once, with no token and no cookie, as anyone could; a page answering 400 or more, or loading apigami.js from /apigami.js on your app’s own address, blocks publication, and publishing asks it again first. A page answering 200 with a body is not refused, but the answer reminds you that anyone can read what it rendered on the server. A deployment answers with a publish block carrying publishable, what is blocking and the one sentence to act on, so you do not have to ask a second time.

Deploying and publishing are one route, PATCH /v1/apps/{app_id}/versions/{version_id}, whose body takes deployed: true, production: true or both, and nothing else. Sent together, the deployment comes first, but a version deployed for the first time has never been run, so the publication answers 409: run an action between the two. With the MCP tools, app_deploy and app_publish take app_id and version_id, and app_publish on an earlier version is the rollback. The answer is the version: its state, production, health, tried_at, review and a publish block with publishable, its blockers and the next step.

An application that is unlisted or public goes to an Apigami administrator for review before another group may call it. Publishing it is a request for review: production does not change, for anyone, your group included, and the answer carries "review": { "state": "pending_review" } with a note. An administrator then puts the version in production, or answers rejected with a reason. Every version you publish is reviewed, a rollback included; a private application is published at once. app_status (GET /v1/apps/{app}) follows the review, and you can write to contact@apigami.com about it.

Who may connect is the application’s visibility: private, your group only, the default; unlisted, anyone with its link; public, also listed in the catalogue. Opening it to others is the person’s decision: an assistant asks before raising it, and never lowers it on its own. It is app_settings with app_id and visibility, or:

Terminal window
curl -s -X PATCH "https://api.eu.apigami.com/v1/apps/$APP_ID" \
-H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' -d '{"visibility":"unlisted"}'

It answers the application. POST /v1/apps takes the same field at creation, {"name":"Watch","visibility":"private"}; any field these two routes do not know answers 400. Once the application is unlisted or public, each publication is a request for review, as above. A version already in production when you raise it, published while the application was private, was never reviewed: until a reviewed version replaces it, it keeps serving your group alone, outside the catalogue, and anyone else is refused with 403 app not reviewed. Publish a new version to ask for that review.

Try an action that reaches a site through the browser

Section titled “Try an action that reaches a site through the browser”

An action that uses a site session runs in one of your user’s browsers. With your application’s key, name the user with X-Apigami-User-Ref, their user_ref, and to pick one of their browsers add X-Apigami-Install-Ref; without it, Apigami takes the browser POST /call would take for the first site the version reaches through a browser. Both are headers, because the body and the query string are your action’s own. Under a run token neither is accepted: the run already names its browser. The same holds for the body of POST /call: with your application’s key, user_ref is required and install_ref optional; when your action calls it with the token handed in X-Apigami-Run, leave both out, since naming one answers 400.

The first time you try such an action on a site, the person may see Apigami’s approval window for that site, even just after connecting to your application: the connection approves your sites in the same click only when it attached their browser in Apigami’s window (Site approval). Tell them to expect it and to click Authorize; until they answer, the action’s POST /call gets 403 with consentPending: true, and running it again afterwards goes through.

Three places to look, and they answer different questions:

  • GET /v1/apps/{app}/logs?version=… — what a deployed version printed. A version whose deployment was refused printed nothing there: the refusal says why.
  • GET /v1/runs/{run} — what the invocation returned: its state, the status your code answered, which environment served it, and the lines a trial kept when a sandbox ran it.
  • GET /v1/apps/{app}/runs?version=… — what ran and what failed, including a trigger that fired with nobody watching.

What your code wrote to its own database is read with POST /v1/apps/{app}/database/queries: Apigami is not on that path.

A deployment that failed: its build, or its server at startup

Section titled “A deployment that failed: its build, or its server at startup”

A deployment that could not be brought up answers 422, with the step that failed and what it printed:

  • the build: an import that does not resolve, a package refused, with the end of the build’s log;
  • the migrations: the file and the statement that failed (Migrations);
  • the code as it loads: input or output at the top level of a file, an exception thrown there, or [apigami] Export default { fetch(request, env, ctx) } from your entrypoint.

Nothing ran, so GET /v1/apps/{app}/logs?version=… (app_logs) has nothing from it. Run one of its actions with X-Apigami-Version: a version with no deployment runs in a sandbox, whose answer and run (GET /v1/apps/{app}/runs, app_runs) carry the same failure and what the code printed. A version written for manifest v1 is refused with 409 before any of this (Retired fields).