Skip to content

Go to the docs contents

The API

The while REST API is for CIs, scripts and tools that don’t speak MCP. This page lists its 265 operations.

Updated November 9, 2026.

View as Markdown

List your projects
curl -H "Authorization: Bearer $WHILE_TOKEN" https://app.while.sh/v1/projects

The address and the format

The API answers at https://app.while.sh, in JSON. Its full description, in OpenAPI 3.1 format, is at https://app.while.sh/openapi.json: you can generate a client from it.

Authenticate

Every call carries a token in the Authorization: Bearer header. A project token is for a CI or a script; an organization token sees the organization’s projects that its person sees; a service account key (whcs_…) acts with exactly its permissions. A project or organization token never goes beyond its person’s permissions. The Tokens and Permissions and approvals sections say how to create them.

Three exceptions: the git repository takes the token as a password (Basic, any user name), Sentry SDKs authenticate with the key in their DSN, and OTLP telemetry with the key in its address. The MCP server is at https://app.while.sh/mcp.

Errors

An error follows RFC 9457 (application/problem+json): type, title, status, detail. A refusal (403, 409, 428, 429) also says what is blocking (why), what to do next (what_to_do) and, when there is one, the ready-made call (action): the MCP tool, the API request or the link to open, and who must do it, the agent, a person or the while team.

Approvals

A sensitive action, like destroying production or making an app public, can require a person’s approval. The response is then 428, with the request and its link. A person approves it in while; then send the same call again with approval_id. A token never approves a request.

Without a token

What you can read without authenticating: pricing, the building block catalog, the MCP tools, the while.yaml schema, while’s network addresses, its version and the API description.

Read while’s version

GET /v1/info

Returns while’s version and its public addresses. With a token, the answer adds the engine and the bricks, without their internal addresses.

Read the catalog of bricks

GET /v1/catalog

Returns an app’s contract and the bricks a while.yaml can declare, each with its state: available, partial or planned.

Read while’s network addresses

GET /v1/network

Returns while’s entry point (its name, for a subdomain’s CNAME or a bare domain’s ALIAS, and its IPv4 and IPv6 addresses, for a bare domain’s A and AAAA records as a last resort only: they can change) and the IPv4 addresses through which apps go out (to declare to a third party that filters by address). Each with the day it has applied since, when stated, and an announced change: the day and the new addresses. No token. The response is cached for five minutes, and one network can read it 60 times a minute.

Read the public pricing

GET /v1/pricing

Returns the price list in force (its version and date), its plans and their prices, and the price of each resource as the Billing page shows it. No token. The answer is cached five minutes, and a given network can read it 60 times per minute.

List the MCP tools

GET /v1/mcp/tools

Returns exactly what tools/list serves an organization’s agent on /mcp, or, with server=trial, on the trial server /essai. No token. The answer is cached five minutes, and a given network can read it 60 times per minute.

Parameters: server.

Read the while.yaml schema

GET /schema/while.v1.json

Returns the JSON Schema of while.yaml, the file that describes an app. Your agent uses it to check its file before deploying.

Read the OpenAPI description

GET /openapi.json

Returns the API description, in OpenAPI 3.1 format.

Projects

Create, read and destroy a project, list an organization’s apps with their metrics, and change a project’s owner or budget.

List an organization’s apps, with their measures

GET /v1/teams/{slug}/apps

The apps your identity sees in this organization. For each, in production: the live version (version.number, version.since), the latest build when nothing is live (build), open incidents, who its services are open to (open), its requests over the last two hours in 5-minute buckets, oldest first (traffic), the time under which 95% of responses left (p95_ms), its tasks and their last run, and the organization’s pause (stop); what it has cost this month, in euros (null without a measurement).

List the projects

GET /v1/projects

Returns the projects your identity sees.

Parameters: organization.

Create a project

POST /v1/projects

Creates a project in your organization, with a monthly budget of €20 by default. An uncapped budget or one above €20, and the sandbox, require an admin of the organization and the approval of another person: the project is then created with the default budget, and the 202 answer carries the request. An admin does it without waiting for anyone, recorded in the log (their agent, at the floor, has them confirm it), unless the organization requires two approvers.

Body: slug (required), name, budget_eur, account, sandbox, team.

Read a project

GET /v1/projects/{slug}

Returns the project if it is visible to you, otherwise 404.

Destroy a project

DELETE /v1/projects/{slug}

Destroys the project with its environments, its git repository and its sources, in the background (202). It takes permission to destroy the project. An agent token always asks a person (428, with the request); a production that holds data needs an admin’s approval, and an admin destroys it themselves in while.

Parameters: approval_id.

Read a project’s history

GET /v1/projects/{slug}/history

Returns the project’s versions, plans and events, mixed, from the most recent to the oldest.

Parameters: limit.

Change a project’s owner

PUT /v1/projects/{slug}/owner

Reserved for admins and the current owner.

Body: email (required).

Change a project’s budget

PUT /v1/projects/{slug}/budget

Reserved for admins. Lowering the monthly budget applies right away; raising it is confirmed or approved (202, with the link).

Body: budget_eur (required).

Tokens

Project or organization tokens, for a CI or a script, and the git identifier of an agent connected with OAuth.

List a project’s tokens

GET /v1/projects/{slug}/tokens

Returns the project’s tokens, without their value. Reserved for project administration: a project token does not list them.

Create a deployment token

POST /v1/projects/{slug}/tokens

Creates a token for an external CI or an agent. It requires the access.grant permission on the project. The token acts for you: its permissions never exceed yours on this project, and are recomputed at each request.

Body: name, days, scopes, environments.

Revoke a token

DELETE /v1/tokens/{id}

The token’s person can always revoke it, in their session; otherwise it takes permission to grant access on its scope (its project or the organization). A refusal answers 404, like a token that does not exist.

Create an organization token

POST /v1/teams/{slug}/tokens

The token sees the organization’s projects that you see, and only those. Its value is shown only once.

Body: name, scopes, days, environments.

Set a token’s expiry

POST /v1/tokens/{id}/expiry

30 days by default. The call can only bring the expiry closer.

Body: days.

Get a git identifier for an OAuth agent

POST /v1/projects/{slug}/git

An agent connected through OAuth does not create tokens: this call gives it a git identifier valid one hour, for this project only, with the permissions its person consented to. It is used for git push and git fetch, on branches only, without tags, without rewriting or deleting a branch; the API and MCP refuse it. It is cut when the agent is disconnected, or when its person can no longer deploy, and five at most are active at once. Pass it in the Authorization: Bearer header, never in the address.

while.yaml and plans

The file that describes the app, and the plan that says what will change before it is applied.

Read the current while.yaml

GET /v1/projects/{slug}/intent

Returns the file that describes the app, as while last received it.

Parameters: environment.

Upload a while.yaml

PUT /v1/projects/{slug}/intent

Stores the file that describes the app, once validated by the while.yaml schema.

Parameters: environment.

Compute a plan

POST /v1/projects/{slug}/environments/{env}/plan

Returns what will change in the environment, without applying anything. Without intent or from_state: the last while.yaml uploaded.

Body: intent, from_state.

The organization’s plan

GET /v1/teams/{slug}/plan

For its administrators. Its plan (what it includes, usage against its limits, its options), the plans to compare, the prices and what is included; the payment method when one is required (its state, the card’s last four digits) and the reason for a pause.

Read a plan

GET /v1/plans/{id}

Returns a plan’s state.

Apply a plan

POST /v1/plans/{id}/apply

Applies the plan’s changes. A deletion in production, or of a database, requires a human approval: the answer is 428, with the link to the request.

Body: approval_id.

Discard a plan

POST /v1/plans/{id}/discard

Discards a plan you will not apply. You need to be able to deploy on the plan’s environment. A plan already applied or discarded returns 409.

Read an app’s diagram

GET /v1/projects/{slug}/environments/{env}/schema

What the app is made of and how it is wired, taken from its while.yaml: its entry points (the internet, the team), its services, its tasks, its databases, its caches, its storage, the other apps’ databases it reads, its data sources and its backups (bricks), and their links (links: from, to, kind). With what while measures and status does not say: each service’s requests over two hours in 5-minute buckets (series_2h) and their response time, each task’s runs today, the sources’ calls over 24 h, the latest backup and its restore test. Each brick’s state is read in status.

Versions and deployments

Sources, versions, rollbacks and the state of an environment.

Mettre en ligne

POST /v1/projects/{slug}/environments/{env}/deploy

Deploys a source archive (tar.gz), an image already built, or, with an empty JSON body, the last commit of the project’s sources at while, written by POST …/files or pushed through git. If this commit is already live, nothing is rebuilt (200); if a version of this commit is in progress, it is returned as is. Otherwise, a new version starts (202).

Body: image, digest, intent.

List the source files

GET /v1/projects/{slug}/files

Returns the files of the project’s sources at while, those of its git repository: paths, sizes and last commit.

Parameters: path.

Write source files

POST /v1/projects/{slug}/files

Writes the named files in a commit on the project’s branch; the others do not change, and delete removes files or folders. UTF-8 text, 2 MiB and 200 files at most per call. If nothing changes, no commit is created (200); if another commit arrives during the write, the answer is 409. Then POST …/environments/{env}/deploy without an archive deploys this commit.

Body: files, delete, message.

Read a source file

GET /v1/projects/{slug}/files/{path}

Returns the content of a text file, 256 KiB at most. A binary file is flagged, without its content.

Download a version’s sources

GET /v1/projects/{slug}/environments/{env}/releases/{number}/sources

Returns the tar.gz archive the version’s build read. It is kept 7 days, and as long as the version is live.

List the versions

GET /v1/projects/{slug}/environments/{env}/releases

Returns the history of the environment’s versions.

Parameters: limit.

Redeploy a built version

POST /v1/projects/{slug}/environments/{env}/releases/{number}/redeploy

Creates a new version with the same image and today’s variables, without rebuilding anything. When the organization’s policy requires an approval, the answer is 428 with the link; a hardened production deployment stops before being applied and resumes on its own once approved.

Read a version

GET /v1/releases/{id}

Returns the version, with its build and its plan.

Remove a version from its bindings

POST /v1/projects/{slug}/environments/{env}/releases/{number}/remove-for-binding

For OIDC federation, with the body { reason }. A person who approves for the project, through their session. The version will never sign again, even if it comes back through a rollback.

Body: reason (required).

Roll back to a previous version

POST /v1/projects/{slug}/environments/{env}/rollback

Deploys a previous version again. When an approval is awaited, the answer is 428 with the link to the request, as for POST …/deploy, and a CI stops there; send the call again with approval_id once the request is granted. Through an agent upload link (POST /v1/upload/{token}), the answer is 202 with what follows; through the MCP tool, “approval required”.

Body: to, approval_id.

Restart a service

POST /v1/projects/{slug}/environments/{env}/services/{service}/restart

Restarts a service of the live version, without creating a version, and cuts the requests in progress. A web service’s instances are recreated; a worker restarts in a rolling way. 409 if nothing runs.

Read an environment’s state

GET /v1/projects/{slug}/environments/{env}/status

Returns the live version, its address, its health and the traffic of the last 15 minutes (p50, p95, p99). services[].title and tasks[].title give the while.yaml title, null without a title. For each brick, the answer gives what it reserves and what it uses, measured (null as long as it is not measured).

Parameters: probe.

Read what was applied

GET /v1/projects/{slug}/environments/{env}/state

Returns the environment’s last 20 state versions: what was really applied.

Create a preview

POST /v1/projects/{slug}/previews

Creates a preview environment. It requires permission to deploy.

Body: name, branch, from, copy_database, ttl_hours.

Destroy an environment

DELETE /v1/projects/{slug}/environments/{env}

It takes permission to destroy the project (an admin; not a developer). Destroying production needs human approval: the response is 428 with the link, and you then resend the call with approval_id.

Body: approval_id.

Read a build

GET /v1/builds/{id}

Returns a build’s state: its times, its cache, warm or cold, and its tests.

Read a build’s log

GET /v1/builds/{id}/logs

Returns the log as JSON, or as an SSE stream with the Accept: text/event-stream header.

Parameters: stream, since.

POST /v1/upload/{token}

Receives the source archive of a deployment. The link comes from the MCP tool upload_source and is valid 15 minutes.

Without a token.

GET /v1/sources/{token}

Returns the tar.gz archive of a version’s sources. The link comes from the MCP tool read_source and is valid 10 minutes.

Without a token.

Git and forges

Each project’s git repository, and the link with GitHub or GitLab.

Read the git repository’s help

GET /git/{repo}

Returns the commands to add the repository as a remote and push to it.

Without a token.

GET /git/{repo}/info/refs

The smart git protocol. git-receive-pack requires permission to deploy. A project already at its storage limit (sources and git) receives 409, with the quota refusal as text, which git displays.

Parameters: service (required).

Fetch a git repository

POST /git/{repo}/git-upload-pack

git fetch and git clone (stateless-rpc). Raw or gzip body (Content-Encoding); 413 beyond a source archive’s limit, 64 MB, decompressed included.

Push to a git repository

POST /git/{repo}/git-receive-pack

git push: it requires permission to deploy. A push on main deploys to production; on another branch, it opens a preview. Raw or gzip body (Content-Encoding); 413 beyond a source archive’s limit, 64 MB, decompressed included.

GET /v1/projects/{slug}/forge

Returns the repository linked to the project, without its token.

POST /v1/projects/{slug}/forge

A push on the default branch deploys to production; a merge request opens a preview, commented on the request; each commit receives its status. The forge’s token is checked with it, stored in the vault, and used to create the webhook.

Body: forge (required), repository (required), token (required), base_url.

Disconnect the repository

DELETE /v1/projects/{slug}/forge

Removes the link, and the webhook at the forge.

An environment’s domains, a project’s main address, and the links that show an app to someone without an account.

List the domains

GET /v1/projects/{slug}/environments/{env}/domains

Returns the environment’s domains: status, DNS records to create, while’s finding and certificate.

Add a domain

POST /v1/projects/{slug}/environments/{env}/domains

Adds a domain to a service (the only one with an address when service is absent): the while.yaml changes and a new release goes out (status unchanged, without a release, when the service already has this domain). The domain then waits for its DNS record (GET domains says so).

Body: domain (required), service.

Verify a domain

POST /v1/projects/{slug}/environments/{env}/domains/{host}/verify

Verifies a custom domain right away: its TXT proof and where it points.

Give the project address to another service

PUT /v1/projects/{slug}/principal-address

Under an apps domain without a region: the address of the project alone moves to this public web service, in every environment online (the version online is redeployed as is). A person only, through their session: the app’s owner or an admin; a token gets 403. Without confirm, 409 and the preview: the addresses that leave and those that arrive.

Body: service (required), confirm.

GET /v1/projects/{slug}/environments/{env}/links

Returns the environment’s access links: name, due date, password or not, sign-ins. The link itself is never returned.

POST /v1/projects/{slug}/environments/{env}/links

An access link shows the app to someone who has no while account. Body: { name, days, password }, with days from 1 to 90 and an optional password of at least 8 characters. The link is returned only once: url, and urls, one per address of the environment. In production, it is born inactive and activates when another person approves it (approval: the request). You need to be a person, with project.configure: an agent token receives 403, and the MCP tool request_access_link returns the while page to open. An admin does it without waiting for anyone, recorded in the log (their agent, at the floor, has them confirm it), unless the organization requires two approvers.

Body: name (required), days (required), password.

DELETE /v1/projects/{slug}/environments/{env}/links/{id}

The link stops working right away. You need to be a person, with project.configure.

POST /v1/projects/{slug}/environments/{env}/buckets/{name}/links

Returns a presigned link to an object, valid 15 minutes by default and 24 hours at most. An upload link is for a PUT and requires data.write.

Body: key (required), upload, minutes, upstream.

Variables and secrets

An environment’s variables, and the vault, compatible with Vault.

List variables

GET /v1/projects/{slug}/environments/{env}/variables

The names set in the environment (secrets, plain variables, file secrets and build secrets), never their values.

Set variables

PUT /v1/projects/{slug}/environments/{env}/variables

Sets several variables at once, without touching the others: in the vault by default, in clear with secret: false. They reach the app at the next deployment.

Body: values (required), secret, file, build.

Delete a variable

DELETE /v1/projects/{slug}/environments/{env}/variables/{name}

Deletes a variable or a secret of the environment, and records it in the log; on a project linked to a federated connection, production goes through its approvers (428). With ?build=1, a build secret. The change applies at the next deployment.

Parameters: build.

Check the vault

GET /vault/v1/sys/health

Compatible with Vault’s API.

Without a token.

Read a secret

GET /vault/v1/secret/data/{path}

Compatible Vault KV v2.

Parameters: project, environment.

Write a secret

POST /vault/v1/secret/data/{path}

Compatible with Vault KV v2. It requires permission to deploy.

Parameters: project, environment.

Body: data (required).

Delete a secret

DELETE /vault/v1/secret/data/{path}

Compatible with Vault KV v2. It requires permission to deploy.

Parameters: project, environment.

List the secrets under a prefix

GET /vault/v1/secret/metadata/{path}

Compatible Vault KV v2.

Parameters: project, environment.

Databases and buckets

Read a database in SQL, its backups and its queries, and the files in the buckets.

List the backups

GET /v1/projects/{slug}/environments/{env}/backups

Returns the backups of the environment’s databases, with the result of their restore test. status: running, succeeded, failed, or expired (its objects were erased at expired_at: it can no longer be restored).

Restore a backup

POST /v1/projects/{slug}/environments/{env}/restore

Restores a backup of a database, in a single transaction, after a safety backup. Requires a human approval.

Body: database, backup_id, instant, approval_id.

Restore a cut connection

POST /v1/teams/{slug}/connections/{connection}/restore

It requires the same right as cutting it. The source resumes as it was: phase, rules, permissions. No effect on an open source. No MCP tool.

Read a database with SQL

POST /v1/projects/{slug}/environments/{env}/query

Runs a single read-only SQL query (SELECT, WITH, VALUES, TABLE), capped, noted in the activity log without its values. It runs on behalf of the person, under the PostgreSQL role of their identity (while_id_…), which only has their permissions on the data: data.read on the database, a schema, a table or columns. READ ONLY transaction; 10 seconds by default (30 at most), 400 rows by default (2,000 at most), 512,000 bytes by default. Refused (400): several statements, a write, DDL, COPY, DO, functions that act, and what builds a large value in memory (repeat, lpad, rpad or format beyond 10,000 characters, array_fill, generate_series beyond 100,000 values, an aggregation over a larger series). Without the permission, PostgreSQL refuses: 403, with the table, the readable columns and the permission (missing_permission, object, readable_columns). A database that does not yet have the while_acces role, before its next deployment, returns 409; another refusal by the database, 422. A database that declares replica: true is still read on the primary, read-only, and the answer says so.

Body: sql (required), database, limit, max_bytes, timeout_seconds.

Export a query as CSV

POST /v1/projects/{slug}/environments/{env}/export.csv

Exports the result of a single read-only SQL query, with the same role, the same refusals and the same activity log as POST …/query. 10,000 rows and 1,000,000 bytes by default (50,000 and 4,000,000 at most), 10 seconds (30 at most). A header then the rows, in CRLF; NULL is empty, an empty string is written "". A cell a spreadsheet would read as a formula (=, +, - or @ first, numbers aside) is preceded by an apostrophe. The x-while-lignes and x-while-tronque headers (non, rows or bytes) say what was returned. 429 when the organization already has 3 reads in progress.

Body: sql (required), database, limit, max_bytes, timeout_seconds.

Read a bucket

GET /v1/projects/{slug}/environments/{env}/buckets/{name}/objects

Reads a bucket through while: the list of its objects, 200 per page, or, with ?key=, the beginning of a text object (64 KiB). It requires data.read.

Parameters: key, prefix, next.

Renew the S3 key

POST /v1/projects/{slug}/environments/{env}/buckets/rotate-key

Creates a new S3 key for the environment. It is used at the next deployment, and the old one holds until then; with immediate, the old one is deleted right away.

Body: immediate.

Read a database’s SQL queries

GET /v1/projects/{slug}/environments/{env}/databases/{database}/queries

Returns the queries the app sends to its database, according to pg_stat_statements: the most frequent and the slowest. The MCP tool observe_app (view database) returns the same.

Parameters: limit, window_minutes.

Scheduled jobs

Each environment’s jobs and their runs.

List the scheduled jobs

GET /v1/projects/{slug}/environments/{env}/tasks

For each task: its title (that of the while.yaml, null without a title), its schedule, its last run (state, duration, error, what to do), the run in progress, the next one and the last 14.

List a task’s runs

GET /v1/projects/{slug}/environments/{env}/tasks/{task}/runs

Returns the history of runs: trigger, author, arguments, start, end, state, exit code, log excerpt and Job.

Parameters: limit.

Run a task now

POST /v1/projects/{slug}/environments/{env}/tasks/{task}/runs

Starts a manual run, or the catch-up of a period (WHILE_CATCH_UP_START and WHILE_CATCH_UP_END). Refused if a run is already in progress.

Body: args, env, catch_up.

Observe

The app’s logs, metrics, traces, errors and events, and what agents did there.

Read the traces

GET /v1/projects/{slug}/environments/{env}/traces

Returns the app’s most recent OpenTelemetry traces, or a trace by its trace_id. They are kept 30 days. The MCP tool observe_app (view traces) returns the same.

Parameters: trace_id, service, operation, since_minutes, min_duration, errors, limit.

Read an environment’s threats

GET /v1/projects/{slug}/environments/{env}/security

Returns what threatens the environment: the vulnerabilities of its dependencies (UpdateFOMO), its exposure (Probr), the bots that call it (Polygraphe) and the findings of its build (Gitleaks, Opengrep). The MCP tool get_app_status returns its vulnerabilities and bots (include: ["vulnerabilities"]).

Parameters: since.

Download the live version’s SBOM

GET /v1/projects/{slug}/environments/{env}/sbom

Returns the CycloneDX SBOM of the environment’s live version, taken from its lockfiles, as a file to download; 404 when that version has none.

Read the app’s logs

GET /v1/projects/{slug}/logs

Queries the app’s logs in LogsQL.

Parameters: environment, query, limit, minutes.

Read the metrics

GET /v1/projects/{slug}/metrics

Queries the app’s metrics in PromQL, through Prometheus’s HTTP API.

Parameters: query, environment, start, end, step, range.

Read the events

GET /v1/projects/{slug}/events

Returns the project’s events in CloudEvents format. With stream=1, or the Accept: text/event-stream header, the answer is an SSE stream. The stream checks your identity again (the token or the session, with project.view) at each event and every 60 seconds, and closes as soon as it no longer passes: member removed, token revoked, session closed.

Parameters: limit, stream.

Read the app’s errors

GET /v1/projects/{slug}/errors

Returns the app’s errors grouped by fingerprint, 50 groups at most: the number of occurrences, the first and last time, the declared version.

Parameters: environment, since.

Read the usage as CloudEvents

GET /v1/usage/events

Returns the usage of the projects you see, as CloudEvents in OpenMeter format.

Read what each service consumes

GET /v1/projects/{slug}/environments/{env}/resources

Each service’s processor (vCPU), memory (bytes) and number of replicas over the last hours (1, 6, 24 or 168), about a hundred points per curve ([instant in ms, value]). A sleeping service has no point. measure: false when the measurements did not answer. What each service reserves is read in status (sizes).

Parameters: hours.

Read traffic over time

GET /v1/projects/{slug}/environments/{env}/traffic

Per slot (1 min over 1 hour, 5 min over 6 hours, 15 min over 24 hours, 2 hours over 7 days): the requests, those that failed, the median and 95th percentile response time in ms, and the release that served the most. With the deployments and incidents of the range, and the error threshold of the default alert in % (null when muted).

Parameters: hours.

Read the timeline

GET /v1/projects/{slug}/timeline

Returns on a single line while’s events, new errors and the agents’ actions through MCP.

Parameters: environment, since, until, families, limit.

Read a version’s impact

GET /v1/projects/{slug}/environments/{env}/impact

Compares traffic, errors and latency before and after the version, and returns a verdict and proposals.

Parameters: version, window_minutes.

Ask a question about production

POST /v1/projects/{slug}/environments/{env}/ask

A question like “why has it been slow since 2 pm?”. Returns the facts, the suspects and proposals.

Body: question (required).

List the agents’ actions through MCP

GET /v1/projects/{slug}/mcp-actions

Returns what the agents did through MCP: the tool, its parameters (sensitive values masked) and the result.

Parameters: since, limit.

Read the telemetry entry points

GET /v1/projects/{slug}/telemetry

Returns the project’s Sentry DSN and OpenTelemetry entry points. The DSN’s key is created at the first read.

Read an error and its stack

GET /v1/projects/{slug}/errors/{fingerprint}

An error grouped by its fingerprint: its occurrences, the first and last time, and the stack of its latest event (function, file, line), with the requested URL.

List the webhooks

GET /v1/projects/{slug}/webhooks

Returns the project’s webhooks, without their secret.

Subscribe a webhook

POST /v1/projects/{slug}/webhooks

Subscribes an address to the project’s events: it takes permission to configure the project, and an https:// URL that resolves to a public address. Deliveries are signed, in CloudEvents 1.0 format, following Standard Webhooks.

Body: url (required), types, secret.

Remove a webhook

DELETE /v1/projects/{slug}/webhooks/{id}

It requires the project.configure permission. Pending deliveries go with it.

Send errors in Sentry format

POST /api/{projectRef}/envelope/

Receives the Sentry SDKs’ envelopes, as is. Authenticated by the DSN’s public key (X-Sentry-Auth, Authorization or ?sentry_key), not by a token. 50 events and 512 KB at most per envelope.

Send OTLP telemetry

POST /otlp/{key}/{environment}/v1/{signal}

Relays the app’s OTLP/HTTP logs and metrics. The project and the environment are imposed on the relayed data; 502 if the collector refuses.

Without a token.

Alerts and incidents

Rules, incidents, channels and the status page.

Read a project’s alerts

GET /v1/projects/{slug}/alerts

Returns the default alerts (state, mute), the written rules (condition in plain words, last state), the open incidents and those of the last 30 days, the channels, and the origin of the availability measure.

Create or change an alert rule

POST /v1/projects/{slug}/alerts/rules

By its name: a source, a structured condition (neither PromQL nor LogsQL), a window, a severity, what to do and the channels. Noted in the activity log.

Body: name (required), source (required), condition, window_minutes, severity, message (required), channels, environment, active.

Delete an alert rule

DELETE /v1/projects/{slug}/alerts/rules/{rule}

By its identifier or its name. Its open incidents are closed.

Mute a default alert

PUT /v1/projects/{slug}/alerts/defaults/{default}

Mutes, or re-enables, a default alert for this project, one of: task, unreachable, restarts, errors, latency, budget, vulnerabilities, disk.

Body: mute (required).

List the incidents

GET /v1/projects/{slug}/incidents

Returns the open or acknowledged incidents, and those closed in the last 30 days: what, since when, what to do, and the deliveries.

Acknowledge an incident

POST /v1/projects/{slug}/incidents/{id}/acknowledge

Says that someone is on it. The incident closes on its own when things are back to normal. Noted in the activity log.

Body: reason.

Read the status page

GET /v1/projects/{slug}/status-page

Returns the status page’s data (/statut/<organization>/<project>): open to the public or not, state, availability over 30 days, response time and recent incidents.

Open or close the status page

PUT /v1/projects/{slug}/status-page

public: true opens it, false closes it; it is closed by default. Open, everyone reads the displayed name, the state and the current incidents there, without technical detail. Noted in the activity log.

Body: public (required).

List the alert channels

GET /v1/teams/{slug}/alert-channels

Returns the organization’s channels, without their addresses, which are secrets, and the agents’ wake-up, always active.

Add an alert channel

POST /v1/teams/{slug}/alert-channels

Reserved for admins. An email (the address of a member of the organization, otherwise 403), Slack (the incoming webhook’s address) or a generic webhook (signed CloudEvents; the signing key is returned only once). 10 channels per organization; beyond that, 409 quota-exceeded.

Body: type (required), name, address, url, min_severity.

Remove an alert channel

DELETE /v1/teams/{slug}/alert-channels/{channel}

Reserved for admins. Its address is destroyed in the vault.

Test an alert channel

POST /v1/teams/{slug}/alert-channels/{channel}/test

Sends a test message through this channel. Reserved for admins; one per minute and per channel, otherwise 429 with Retry-After.

Read an app’s status page

GET /v1/status/{organization}/{project}

An app’s public status page, without an account when its project opened it: the display name, the state, ongoing incidents, without technical detail. Whoever sees the project reads all of it (each address, its 30 days, whether the page is open). Otherwise 404, the same answer as for an unknown app.

Without a token.

Internal apps

Who signs in to an app restricted to the organization, its agent’s access, and the key that signs their identity.

List who gets into the internal apps

GET /v1/projects/{slug}/sign-ins

Returns, for each internal app of the project, the rule that says who can get in, and who got in, with the date. You need to be able to read the project’s activity log.

List the agents’ accesses to internal apps

GET /v1/projects/{slug}/app-access

Returns the active identifiers your agents opened to get into an app of the project. The identifier itself is never returned, only its id, to cut it.

Open an internal app to a person’s agent

POST /v1/projects/{slug}/app-access

Gives a person’s agent a one-hour identifier to get into an internal app (access: internal). The agent goes through its OAuth connection or a token of its person; a service account or a session is refused. The app receives it as the person: same permissions, its rule read again at each request, and while_agent in the X-While-Identity header. The identifier holds for this address only, five at most active per person, and goes in the Cookie header (__Host-while-agent=…), never in the address. With read_only, only GET and HEAD go through. It is cut when the agent is disconnected, or by DELETE. When the person’s role requires an approval to “Open an internal app through their agent”, the answer is 428 with the request; then send the call again with approval_id.

Body: service, environment, address, read_only, approval_id.

Cut an agent’s access to an app

DELETE /v1/projects/{slug}/app-access/{id}

Cuts the identifier right away. Its person or an admin of the organization can do it.

Read the identity tokens’ key

GET /.well-known/jwks.json

Returns the public key (JWKS, ES256) that signs the X-While-Identity identity token passed to internal apps.

Without a token.

Connections

The external APIs that apps call through while: their rules, their secrets, their limits and their log.

List the connections

GET /v1/teams/{slug}/connections

Returns the organization’s connections: phase (declaration to approve, declared, learning, locked), authentication (without secret), calls and refusals of the last 24 hours, and the <NAME>_URL variable.

Declare a connection

POST /v1/teams/{slug}/connections

The declaration is approved by an approver, or goes through in your name when the organization’s rules leave you autonomous (201). It is made without the secret, entered afterwards with PUT …/secret. The connection starts in learning for 7 days: writes are blocked there, except POST reads (post_read).

Body: name (required), label, domain, model, secondary_domains, authentication, post_read, rules, learning_days, rate_per_minute, owner, project, apps, direction.

Read a connection

GET /v1/teams/{slug}/connections/{connection}

Returns its rules (its surface: method, pattern, approval at each call); who can call it (connection.read and connection.write assignments, as sentences); who called it this week, and through which assignment; the week’s refusals; the changes of rules and access; the observed calls and the locking proposal; the last calls (metadata) and the held calls; the approvers, and how to call it.

Enter a connection’s secret

PUT /v1/teams/{slug}/connections/{connection}/secret

Whoever can configure the connection (an admin, or an assignment that grants it) enters or changes the secret. It is stored in the vault, never returned nor noted in the activity log; a declared connection then enters learning. The fields follow the recipe: value; user and password (basic); client_id and client_secret (oauth2); identifier and password (session).

Read the locking proposal

GET /v1/teams/{slug}/connections/{connection}/lock

Returns, without requesting anything, the rules while computes from the observed calls (identifiers generalized as *) and the diff. Who can call stays a matter of assignments.

Lock a connection to what was used

POST /v1/teams/{slug}/connections/{connection}/lock

while computes the rules from the observed calls and creates the approval request, with the diff; an admin waits for no one: locked at once (200). Once approved, any call outside the rules receives 403 (missing rule), and the caller is notified.

Cut a connection

POST /v1/teams/{slug}/connections/{connection}/revoke

The emergency action of a person answerable for it (connection.configure), without approval. The relay then refuses any call to the source (code “coupee”), even before permissions. No effect on a source already cut. No MCP tool.

List the call limits

GET /v1/teams/{slug}/call-limits

Returns the call limits per caller: those of a source (?connection=) and those of the whole organization, which also hold for it; without a connection, all. For each: the caller, the window, the number, the calls of the current window and when it restarts. It requires org.view.

Parameters: connection.

Set a call limit

POST /v1/teams/{slug}/call-limits

Sets a limit, or changes its number: a caller, a window (minute, hour, day, month) and a number of calls, on a source (connection) or on the whole organization. The caller is app:<project>/<service>, task:<project>/<task>, person:<e-mail>, agent:<identifier>, service_account:<name> or group:<name>, which counts its members and their agents together. Beyond it, the relay refuses the call (429, code “plafond”, Retry-After); an alert goes out at 80%, another at the first refusal, once per window. It requires connection.configure, and the Enterprise plan for an organization that has a plan.

Body: connection, caller (required), window (required), calls (required).

Remove a call limit

DELETE /v1/teams/{slug}/call-limits/{limit}

It requires the same right as setting it (connection.configure), whatever the plan. Noted in the activity log.

List a federated connection’s bindings

GET /v1/teams/{slug}/connections/{connection}/bindings

For OIDC federation: each binding, with its sub (b:<uuid>), its state (requested, approved, suspended or removed), what it freezes (audience, exchange host, API hosts) and its approvals.

Request a binding to a federated connection

POST /v1/teams/{slug}/connections/{connection}/bindings

Binds a production app, a service or task:<name>, to a federated connection, freezing its parameters and the fingerprint of its rules. The binding requires a strict approval by the connection’s approvers, never by the platform nor forced, and, when the box is checked, that of each version by the project’s approvers, on a production deployment. 409 if the federation is not in service, if the connection is not federated and locked, if the caller already has a live binding, or if the live version has an image without a digest. A binding falls on its own (production version without its caller, environment destroyed, project deleted, connection refused or declared again, removal) and never comes back.

Body: project (required), caller (required), approve_production_deploys.

Resume a suspended binding

POST /v1/teams/{slug}/connections/{connection}/bindings/{binding}/resume

An automatic rollback suspends the binding. This call creates a strict request to the project’s approvers; once approved, the binding resumes if the live version is approved for it and has not been removed from it.

Require approval of each production deployment of a binding

POST /v1/teams/{slug}/connections/{connection}/bindings/{binding}/case

The “Require approval of each production deployment” box, with the body { approve: boolean, reason }. Checking it takes effect right away (200); unchecking it is a strict request to the project’s approvers (202).

Body: approve (required), reason (required).

Remove a binding

DELETE /v1/teams/{slug}/connections/{connection}/bindings/{binding}

For good, with the body { reason }. You need to be able to configure the project or the connection. Noted in the organization’s activity log.

Body: reason (required).

Change a connection’s POST reads

PUT /v1/teams/{slug}/connections/{connection}/post-read

Sends the desired list of paths where a POST is a read. A removed path holds right away: a POST there becomes a write again, which requires connection.write and a rule. An added path is approved, like a rule; an autonomous admin goes through without waiting.

Body: post_read, reason.

Request one more rule

POST /v1/teams/{slug}/connections/{connection}/rules

A rule gives a method, a pattern (* for a segment, ** last for the rest) and approve_each_call; it goes through an approval, except for an admin, who waits for no one: applied at once (200). Who can call is given by an assignment (connection-caller, connection-writer), not by the rule.

Body: method (required), pattern (required), approve_each_call, read, reason.

Read a connection’s call log

GET /v1/teams/{slug}/connections/{connection}/calls

Returns each call and each refusal: when, who and through what, method, normalized path, status, duration, size of the body sent in bytes, reason for the refusal, and the assignment that allowed it (role, given by whom, when); never the content. Also returns the held calls. 7 days by default; beyond 30 days, it requires log.read on the connection. Kept according to the organization’s retention.

Parameters: since, until, refusal, limit.

Read a held call

GET /v1/teams/{slug}/connections/{connection}/calls/{call}

A call of an “approve each call” rule: pending, run (status and answer, bounded), refused or expired. Refused by the relay when run, it carries while’s refusal: refusal, refused_by, message, what_to_do, detail.

Call a connection from a workstation

GET /sorties/{organization}/{connection}/{path}

From a development workstation or a script, with your personal token. while adds the key, applies the rules, relays as a stream (SSE and large uploads included) and rewrites the provider’s addresses to this address. Apps hosted by while call <NAME>_URL instead.

Call a connection from a workstation (POST)

POST /sorties/{organization}/{connection}/{path}

Same rules as for GET. Writes are blocked during learning, and an “approve each call” rule returns 202.

Call a connection from a workstation (PUT)

PUT /sorties/{organization}/{connection}/{path}

Same rules as for GET. Writes are blocked during learning, and an “approve each call” rule returns 202.

Call a connection from a workstation (PATCH)

PATCH /sorties/{organization}/{connection}/{path}

Same rules as for GET. Writes are blocked during learning, and an “approve each call” rule returns 202.

Call a connection from a workstation (DELETE)

DELETE /sorties/{organization}/{connection}/{path}

Same rules as for GET. Writes are blocked during learning, and an “approve each call” rule returns 202.

Discover an organization’s OIDC issuer

GET /oidc/o/{organization}/.well-known/openid-configuration

For OIDC federation: iss is while’s address followed by /oidc/o/<uuid>, with its JWKS, in RS256. Served as soon as a federated connection is declared, even before the first key; 404 without an issuer.

Without a token.

Read the keys of an organization’s OIDC issuer

GET /oidc/o/{organization}/.well-known/jwks.json

Returns the organization’s RS256 public keys, never another’s; kid is the key version. { keys: [] } before the first key, 404 without an issuer, 503 if the keys cannot be read.

Without a token.

Your account and your agents

Your account and its language, your connected agents and their permissions, your announcements, and disconnecting a person’s agents.

Read my notifications: each kind of email or alert, and whether I receive it

GET /v1/me/notifications

Set my notifications

PUT /v1/me/notifications

Body { notifications: { <kind>: true | false } } (alerts, approvals, access_requests, audit_streams); kinds left out stay as they are. From the person’s own session only, never by an agent.

Body: notifications (required).

Read my account

GET /v1/me

Returns the signed-in person: their address, their name if they gave one, while’s language for them (null: their organization’s), platform (true: the back-office session of a named platform administrator) and platform_administrator (true: a platform administrator, who can enter the back-office through SSO). Through their session or a token of their account.

Change my account’s language

PUT /v1/me

Body { language }: one of while’s languages, or empty for their organization’s (400 otherwise).

Body: language.

Read my apps in an organization

GET /v1/me/apps/{organization}

The internal apps the session’s person can open in this organization (each one’s rule, as when opening it), and this organization’s invitations awaiting their consent. An organization where nothing is open to them, or that isn’t theirs, answers like an unknown address: organization is null, without its name; home: their apps in the organization of their company sign-in. From a session only.

List my agents

GET /v1/me/agents

Returns the person’s agents of every kind: agents connected through OAuth, tokens of their account that have been used, clients seen in the activity log through their session. Each with where it works, what it can do (permissions null: the same as the person), when it was last seen, its actions over the last 14 days (read, act), its day hour by hour and the sources it calls. organization parameter (its slug): the agents of that organization, and the number of agents of the other members (other_agents). From the person’s session only.

Parameters: organization.

Read my announcements

GET /v1/me/announcements

The while team’s current announcements for the person. From their session: their banner, minus those they dismissed or acknowledged; ?organization=: the banner of that organization only (404 if they are not a member). From their token: all current announcements of their organization, with acknowledged (their receipt). read_receipt: the announcement asks for a read receipt (POST /v1/announcements/{id}/acknowledge).

Parameters: organization.

Read one of my agents

GET /v1/me/agents/{id}

Returns the agent (its id: o-<family>, j-<token>, s-<fingerprint>), its last 25 actions and its calls to sources, the apps where it acts, its requests in its organization (approvals, access), and the choices for changing its permissions (change_permissions, null for a session or an agent that creates its project). From the person’s session only.

Disconnect one of my agents

DELETE /v1/me/agents/{id}

An agent connected through OAuth (o-<family>, or its family): its tokens are revoked, its permissions remain, it reconnects through OAuth. A token is revoked with DELETE /v1/tokens/{id}. From the person’s session only: a token or an agent gets 403.

Change the permissions of one of my agents

PUT /v1/me/agents/{id}/permissions

Body { level, scope, project }: level read, act or admin; scope app (with project, an app of its organization) or organization (for whoever grants access there; admin only there). Never more than the person can do where it works; takes effect at once, and is recorded in the activity log. An OAuth family or a token of their account, from their session.

Body: level (required), scope (required), project.

Set the tool groups of one of my agents

PUT /v1/me/agents/{id}/tool-groups

Body { tool_groups }: the whole list of MCP tool groups the agent keeps (alerts, feedback_and_docs, environments_and_storage, administer), the ones it loaded with load_tools. A group gives no permission: it only lists its tools. Logged; an OAuth family or a token of your account, from your session.

Body: tool_groups (required).

Disconnect a person’s agents

POST /v1/teams/{slug}/agents/disconnect

Immediately revokes the given access families (agents[].families in the member list); the person keeps their permissions. The person themselves, or an organization administrator; each one is logged.

Body: families (required).

Organization and members

Organizations, their members, invitations, groups and Enterprise Sign-In.

List the organizations

GET /v1/teams

Returns your organizations with your role, or the token’s one. Each comes with its plan, the level of its subscription for the month ({ key, label, since }), or null without a subscription.

Create an organization

POST /v1/teams

Creates an organization from a session. The person who creates it is its owner. payment_method: "to_register" is reserved for the platform, for a disposable test organization (e2e-…, Mollie test mode): it is born without a waiver, its payment method to register.

Body: slug (required), name, payment_method, language.

Read an organization

GET /v1/teams/{slug}

Returns the same record as the list, with your role and the plan. An organization that is not visible to you returns 404.

Change an organization’s language

PATCH /v1/teams/{slug}

Its language (language): the one while uses for its members who have not chosen theirs, for its agents and its emails; its owners and admins change it. An organization cannot be renamed: a body that carries slug or name returns 400, and nothing changes. An organization renamed earlier keeps each old address for 90 days: the API answers there without redirecting, the interface redirects there (302), nobody else takes it.

Body: language, slug, name.

Read the organization’s map

GET /v1/teams/{slug}/card

Returns, as facts, the projects, their services (public or internal), their databases, their tasks and their connections, and who reads which database or calls which connection. Only what you see: nothing of an invisible project, nor of what links it to the others. The MCP tool get_organization { view: "map" } returns the same.

List the people who got in through the enterprise connection

GET /v1/teams/{slug}/enterprise-people

Returns the people who got in through the enterprise connection without being members: address, issuer name, received groups, opened apps and the rule that lets them in, dates and session. They have no right in while.

Close a company person’s sessions

POST /v1/teams/{slug}/enterprise-people/{id}/close-sessions

Closes their sessions in while, in the realm and in the opened apps. They come back as long as an app’s rule lets them through: the answer says which ones, and how to prevent it.

List the members

GET /v1/teams/{slug}/members

Returns the organization’s members: their role, since when, their name, their last sign-in, whether they sign in through SSO, their groups and their connected agents. For administrators, also: the service account keys each one created (revoked if they leave) and when their invitation expires.

Invite a person

POST /v1/teams/{slug}/members

Reserved for owners and admins. The answer is the same whether or not the person already has a while account: 202 { status, email, role, invitation: null, note }. status is “invitation sent”, or “invitation recorded” when no email went out: sending impossible, or a test or example address, to which while never writes. The link goes by email to the invited address, never to who invites. An admin or owner role applies after another person’s approval. 10 invitations of non-member addresses per hour and per organization (429, retry_after_s). An admin does it without waiting for anyone, recorded in the log (their agent, at the floor, has them confirm it), unless the organization requires two approvers.

Body: email (required), role, reason.

Resend an invitation

POST /v1/teams/{slug}/members/{email}/resend

Sends a new link by email to the invited address; the old one no longer works. Once an hour at most.

GET /v1/sso/link

The link received at a while account’s address after its first sign-in through its organization’s company sign-in (member SSO, 30 minutes): the account’s address, the organization and the issuer. A link that no longer works: 410.

Without a token.

Parameters: j (required).

POST /v1/sso/link

The owner confirms, once: the account then signs in through the company sign-in. No session opens here; born from a password session, the link requires that same session. to: the page to go to. From a while page (origin check); a link that no longer works: 410.

Without a token.

Body: j (required).

List the groups

GET /v1/teams/{slug}/groups

Returns the organization’s groups, made in while or coming from the enterprise connection, and their members.

Create a group

POST /v1/teams/{slug}/groups

Reserved for admins.

Body: name (required), sso, enterprise.

PUT /v1/teams/{slug}/groups/{group}/enterprise

Links a while group to a group of the enterprise connection’s issuer, by the group’s stable identifier at the issuer, never by its name; enterprise: null detaches it. Reserved for an admin person, through their session: a token receives 403. Linking lets into the group those the issuer announces there: you therefore need to have yourself what the group gives. A group that administers, or is a designated approver, awaits an approval (428). Noted in the activity log.

Body: enterprise.

PUT /v1/teams/{slug}/groups/{group}/sso

Reserved for admins; sso: null detaches it. At each sign-in through SSO, if the identity token carries the groups claim, the person joins the while groups whose sso appears in it, and leaves those whose sso no longer appears, ignoring case and a leading ”/”. Noted in the activity log.

Body: sso.

Change a member’s role

PUT /v1/teams/{slug}/members/{email}/role

Gives the new role on the organization and removes the old one, like the MCP tools grant_role and revoke_role, and notes it in the activity log. access.grant is enough, since you only give what you have. Naming an admin or an owner awaits another person’s approval (202): until then, the person keeps their role, and the old one goes once approved. The external role removes any role on the whole organization. An admin does it without waiting for anyone, recorded in the log (their agent, at the floor, has them confirm it), unless the organization requires two approvers.

Body: role (required), reason.

Delete a group

DELETE /v1/teams/{slug}/groups/{group}

Reserved for admins. Its approver designations go with it; a group designated approver first requires an approval (428).

Add a member to a group

POST /v1/teams/{slug}/groups/{group}/members

Reserved for admins, for a member of the organization, and not for a group of the enterprise connection. A group that administers, or is a designated approver, first requires an approval (428).

Body: email (required).

Remove someone from a group

DELETE /v1/teams/{slug}/groups/{group}/members/{email}

Reserved for admins. A group designated approver first requires an approval (428).

Remove a person from the organization

DELETE /v1/teams/{slug}/members/{email}

Reserved for admins. Their tokens and connected agents are revoked. Only an owner removes an owner, and the last owner cannot be removed (409).

Read the enterprise connection

GET /v1/teams/{slug}/sso

Reserved for admins. Returns the OpenID issuer, the client, the domains and the groups claim. The client secret is in the vault: only its presence is said (secret: true). domain_proofs says, for each domain, whether it is proven, and the DNS TXT record to create (_while-challenge.<domain>, while-verification=<token>). Without proof, no address of that domain gets in.

Set the enterprise connection

PUT /v1/teams/{slug}/sso

A generic OpenID Connect connection: Keycloak, Google Workspace, Microsoft Entra… while reads its discovery right away (/.well-known/openid-configuration, over https, at a public address). Only the addresses of the given domains get in through this connection, once each domain is proven by a DNS TXT record (domain_proofs). The return address to declare at the issuer is https://app.while.sh/access/sso/callback. Noted in the activity log.

Body: name, issuer, client_id, secret, domains, groups_claim, active, member_sso, member_passwords, provider, tenant_id.

Remove the enterprise connection

DELETE /v1/teams/{slug}/sso

Its secret is destroyed, and the sessions it opened are revoked.

Read where the enterprise connection stands

GET /v1/teams/{slug}/sso/setup

Admins only, even without a connection (connection null): the settings and their provider, the redirect URI to declare with it, the proof of each domain and its TXT record, the approval in progress and who can give it, the linked groups, the apps already open, the person’s last trial, and the members who would lose the organization without a password.

Verify the domains now

POST /v1/teams/{slug}/sso/verify

Reads each domain’s TXT record again right away (at most once every 15 seconds) and returns their proofs. Admins only.

Let members sign in through the enterprise connection

PUT /v1/teams/{slug}/sso/members

Body { member_sso, member_passwords, confirm }: members sign in with their company account, and the password stays allowed for them or not (then only owners keep it). confirm is required when members would lose the organization. An admin person, from their session; recorded in the activity log.

Body: member_sso, member_passwords, confirm.

Try the enterprise connection

POST /v1/teams/{slug}/sso/trial

Returns the provider address where the admin person signs in with their own account; nothing is opened or created. The browser keeps the request (a cookie); on return, while comes back to the enterprise connection page with what the trial saw. From their session only.

Lister mes invitations

GET /v1/invitations

Returns the organizations your account is invited to. From the invited person’s session only, neither token nor agent. An address not yet proven, neither through SSO nor through a link received by email, sees none: address_verified: false.

Accept an invitation

POST /v1/invitations/{organization}/accept

The person joins the organization with the proposed role, given on behalf of who invited them, within the limit of their permissions today (otherwise granted_role: false). An admin or owner role awaits an approval. From a session, with a proven address: a token, an agent or an unproven address receives 403 (adresse_a_prouver: open the link received by email, or sign in through SSO).

Decline an invitation

POST /v1/invitations/{organization}/decline

The invitation disappears, and the organization’s activity log notes it. From a session, with a proven address.

Acknowledge an announcement

POST /v1/announcements/{id}/acknowledge

The person says they took note of an announcement of the while team; it is dated, and its banner closes. A person, through their session or their token; a service account key receives 403. 404 if it is not a current announcement of their organizations; 409 if it does not ask for a receipt. The second time, the first date stays.

Dismiss an announcement for yourself

POST /v1/announcements/{id}/close

The banner no longer shows this announcement to the person. From their session only (403 with a token). An urgent one cannot be dismissed before its end (403); one that asks for a read receipt stays until it is acknowledged (POST /v1/announcements/{id}/acknowledge). 404 if it is not a current announcement of their organizations.

Permissions and approvals

Who can do what, access requests, service accounts, approvers and approval rules.

List identities

GET /v1/teams/{slug}/identities

The people, groups, service accounts and apps that have an assignment, each with its current direct access. Whoever can see the organization sees them all; otherwise, only themselves.

The access grid for data sources

GET /v1/teams/{slug}/access/grid

What each one can do on each data source, from their effective permissions: agents (with their person’s permissions), apps, scheduled jobs and service accounts as rows, sources as columns; for each cell, read and write (on the whole source, or part of its paths) and configure it. All sources for whoever grants access, otherwise the ones they can see.

Change who can open a service

PUT /v1/projects/{slug}/environments/{env}/services/{service}/access

open: public (the whole internet), organization (its members) or people (who: addresses, @domains, groups; members: true to keep the members as well). The while.yaml changes and a new release starts (status unchanged, no release, when the access is already the one asked for); opening to the whole internet or beyond the organisation requires an approval by default (428), which the organisation’s rules can relax.

Body: open (required), who, members.

Read the approval rules and the budget

GET /v1/teams/{slug}/rules

rules is the ordered list of rules, each in plain words: the first that matches wins. budget_eur is the monthly budget (null: no cap). defaults says what holds without a rule: autonomous within the budget, even in production; approval beyond it; approval to make an app public. floor lists what always requires an approval. pending shows a relaxation that awaits its approval.

Change the rules and the budget

PUT /v1/teams/{slug}/rules

Reserved for admins. Sends the whole list of rules, the budget, or both. What tightens applies right away (200, status “applied”). What relaxes, even a single case, or raises the budget (null: no more cap) goes entirely through an approval (202, status “approval required”, with the request). A rule on an action of the floor, such as destroying production data, changing access or the policy, or an “approve each call” call, is refused (400, with the reason). The activity log notes what relaxes and what tightens.

Body: rules, budget_eur, required_approvals.

Read who has access to a project

GET /v1/projects/{slug}/access

Returns each assignment of the project, or of one of its databases or tables, in one sentence: “Lea Martin reads the table sales.orders (warehouse, reporting) until November 30. Granted by Oscar on October 21: Q4 audit.” With ?who=, everything this identity can do; with ?who= and ?permission=, yes or no, and the chain that allows it: assignment, group, role, who gave it, when and why. Others’ permissions are read where you have access.grant.

Parameters: activity, project, environment, database, schema, table, columns, connection, paths, who, permission.

Read who has access to a resource of the organization

GET /v1/teams/{slug}/access

As for a project, on a resource of the organization, or on the organization itself without a resource: who has access, what an identity can do, and whether it can do something.

Parameters: activity, project, environment, database, schema, table, columns, connection, paths, who, permission.

Grant access

POST /v1/teams/{slug}/attributions

Grants an identity a role on a scope, until a date, for a reason, like the MCP tool grant_role. You only grant what you have (access.grant, and each permission of the role on the scope): never to yourself, nor through your agent, nor to a group you are a member of, nor to a service account you use. Without a due date: only for an admin of the scope, otherwise 90 days at most. Granting permission to administer access awaits an approval (202). A refusal (403) says what is missing. Everything goes to the activity log (access family).

Body: who (required), role (required), scope, until, days, reason (required).

Remove an assignment

DELETE /v1/teams/{slug}/attributions/{id}

The assignment no longer counts, right away. The reason is required, and goes to the activity log.

Parameters: reason.

Body: reason.

List the unused assignments

GET /v1/teams/{slug}/attributions/unused

Returns the assignments you can remove, unused for 60 days: neither an allowed action nor a call. The periodic job flags them once, in the activity log. To remove one: DELETE …/assignments/{id}; to keep it: POST …/assignments/{id}/keep.

Keep an unused assignment

POST /v1/teams/{slug}/attributions/{id}/keep

Its 60-day count restarts, without inventing any use, and the reason goes to the activity log. Who can remove the assignment can keep it, never their own.

Body: reason (required).

Export the assignments for an auditor

GET /v1/teams/{slug}/attributions/export

It requires log.read on the organization. As JSON: each assignment (active, expired or removed), its activity log entries (number, fingerprint, previous, hashed content), the head of the chain and its verification. As CSV: one row per assignment, then a “verification” row. The export is noted in the activity log (security family).

Parameters: format.

List the roles

GET /v1/teams/{slug}/roles

Returns the roles provided by while and the organization’s, their permissions in plain words, where they are given and who holds them, and what you can compose.

Read a role

GET /v1/teams/{slug}/roles/{role}

Returns a role, provided or the organization’s, by its key.

Create or change a role

PUT /v1/teams/{slug}/roles/{role}

Composes a role of the organization from the catalog of permissions. It requires roles.define, and you only compose with what you have: each of the role’s permissions on the organization. Changing a held role amounts to giving it again to its holders: never for a role you hold yourself (403), and a held role never gains access.grant, org.admin nor roles.define (409). Everything goes to the activity log (access family), refusals included.

Body: name, description, permissions (required), reason (required).

Delete a role

DELETE /v1/teams/{slug}/roles/{role}

It requires roles.define. Only when no active assignment gives it any more.

Parameters: reason.

Body: reason.

List the access requests

GET /v1/teams/{slug}/access-requests

Returns the requests you can decide, yours, and your assignments that are reaching their due date, with the call that renews them.

Request access

POST /v1/teams/{slug}/access-requests

Requests a permission for oneself: a person, their agent for them, or a service account through its key. Also used to renew one of one’s assignments (renew), or to request a connection’s call for an app (for). Done after a 403 that carries missing: the smallest role, the smallest scope, the ready call. Fields: role, or permission (while takes the smallest role that gives it), scope, reason (required), days (7 by default, 90 at most). Those who can grant it are notified (To handle, email, and their agents on while://access/events); without an answer within 72 hours, it expires. An identical request still open is returned as is (200), and 10 requests at most stay open (429). Noted in the activity log (access family).

Body: role, permission, scope, days, reason (required), renew, for.

Read an access request

GET /v1/teams/{slug}/access-requests/{id}

Returns its sentence, who can grant it, its decision and what you can do with it. Visible to who made it and to who gives access on its scope (access.grant); otherwise 404.

Cancel one’s access request

DELETE /v1/teams/{slug}/access-requests/{id}

Only while it is open.

Parameters: reason.

Grant an access request

POST /v1/teams/{slug}/access-requests/{id}/grant

A person who can give this permission grants it, at most what is requested, and the request becomes a bounded assignment. Never their own request, never through an agent or a service account (403). Fields: role (its permissions among those requested), scope (contained in the requested one), until or days (at most the requested duration, 90 days, always a due date), reason (required). An admin-level request goes to the owners: an admin of the scope who is not an owner denies it, or gives the role themselves, and another person approves it. Granting a renewal extends the assignment. Noted in the activity log.

Body: role, scope, until, days, reason (required).

Deny an access request

POST /v1/teams/{slug}/access-requests/{id}/deny

A person who could grant it denies it, with a reason, which goes to who requested it.

Body: reason (required).

List the service accounts

GET /v1/teams/{slug}/service-accounts

Returns each service account of the organization: its sentence, its permissions, its keys (prefix, due date, last use) and who uses it. Whoever sees the members (org.view) sees them all; otherwise, those they manage (access.grant on their scope). An account reads its own through its key.

Create a service account

POST /v1/teams/{slug}/service-accounts

Creates a non-human identity (dashboard, script, autonomous agent), without permissions or keys. You need to be a person, or their agent, with access.grant on its scope: the organization, or the given project. Its permissions are then given through an assignment (who: service_account:<name>), by another person than those who use it, then a key. Noted in the activity log.

Body: name (required), description, reason (required), project.

Read a service account

GET /v1/teams/{slug}/service-accounts/{account}

Returns its sentence, its permissions as sentences and its keys.

Delete a service account

DELETE /v1/teams/{slug}/service-accounts/{account}

Its assignments are removed (in the activity log), its keys go and its name is freed.

Parameters: reason.

Deactivate a service account

POST /v1/teams/{slug}/service-accounts/{account}/disable

Its keys no longer pass, its permissions no longer count, and its SQL sessions are cut.

Body: reason (required).

Reactivate a service account

POST /v1/teams/{slug}/service-accounts/{account}/reactivate

Its keys work again. You need to already have each of its permissions.

Body: reason (required).

Create a service account key

POST /v1/teams/{slug}/service-accounts/{account}/keys

Creates a whcs_… key, shown only once, always with a due date: 90 days at most, one year for an admin of the account’s scope. The key gives whoever holds it everything the account can do: you need to manage the account (access.grant on its scope) and already have each of its permissions. It acts with exactly its assignments, on the API and MCP, in the Authorization: Bearer header. The activity log notes the creation, never the secret.

Body: name, until, days, reason.

Rotate a key

POST /v1/teams/{slug}/service-accounts/{account}/keys/{key}/rotation

Creates a new key, with the same rules as a creation. The old one still holds for the overlap: 24 hours by default, from 0 to 168.

Body: name, until, days, overlap_hours.

Revoke a key

DELETE /v1/teams/{slug}/service-accounts/{account}/keys/{key}

It is refused from the next request on. The reason goes to the activity log.

Parameters: reason.

List who approves what

GET /v1/teams/{slug}/approvers

Returns, for each resource (organization, projects, databases, connections), its owner and its approvers. Without a designation, the resource’s owner and the organization’s admins approve; a database inherits from its project.

Designate an approver

POST /v1/teams/{slug}/approvers

Designates a person or a group as approver of a resource. Reserved for admins.

Body: resource_type (required), resource, subject_type (required), subject (required).

Remove an approver

DELETE /v1/teams/{slug}/approvers/{id}

Removes an approver designation. Reserved for admins.

Review the access

GET /v1/teams/{slug}/access/review

Reserved for admins. Returns who reads, deploys and approves each resource, the tokens to review and the connected agents. An agent inherits its person’s permissions. A token is “to review” when it does not expire, has not been used for 30 days, or has more permissions than its person (they are then capped).

Read an approval request

GET /v1/approvals/{id}

Returns the request: its level, its fingerprint, its expiry, its approvers, and what you can do with it.

Read the body of a held call

GET /v1/approvals/{id}/body

The body of a held call (“approve each call”), decrypted for a person who can decide this request, signed in to while: never the agent that made it, never a token. Its UTF-8 text (null if binary), capped at 32,000 characters (truncated beyond), its type, its size and its fingerprint (the same as the request’s). Each read goes to the organization’s activity log, without the content. 404 for anyone who cannot decide, and once the request is decided or expired.

Approve a request

POST /v1/approvals/{id}/approve

A request is decided by another person, an approver of the resource, signed in to while: never their own request, nor their agent’s (same account, 403). A token (agent, OAuth or service account) never decides or forces: 403, reason “agent”, with the link to the request to pass on to its person (action.link). Denying and forcing through require a reason (400 without one); forcing through is reserved for the organization’s admins and emits the sh.while.approval.forced event. A request already decided or expired returns 409.

Body: reason.

Deny a request

POST /v1/approvals/{id}/reject

A request is decided by another person, an approver of the resource, signed in to while: never their own request, nor their agent’s (same account, 403). A token (agent, OAuth or service account) never decides or forces: 403, reason “agent”, with the link to the request to pass on to its person (action.link). Denying and forcing through require a reason (400 without one); forcing through is reserved for the organization’s admins and emits the sh.while.approval.forced event. A request already decided or expired returns 409.

Body: reason.

Force a request through

POST /v1/approvals/{id}/force

A request is decided by another person, an approver of the resource, signed in to while: never their own request, nor their agent’s (same account, 403). A token (agent, OAuth or service account) never decides or forces: 403, reason “agent”, with the link to the request to pass on to its person (action.link). Denying and forcing through require a reason (400 without one); forcing through is reserved for the organization’s admins and emits the sh.while.approval.forced event. A request already decided or expired returns 409.

Body: reason.

List the pending requests

GET /v1/approvals

Returns the requests of the projects you see, and those of your organizations according to your role. A project token only sees those of its project.

Parameters: organization.

List an organization’s approval requests

GET /v1/teams/{slug}/approvals

The approval requests of this organization that your identity sees, pending and decided, most recent first (50 at most); each as GET /v1/approvals/{id} returns it: who decided it (decided_by), when (decided_at), its reason (decision_reason). A request that passed without an answer is returned as expired.

Activity log

The organization’s activity log: read it, export it, verify it and send it to a SIEM.

Read the organization’s activity log

GET /v1/teams/{slug}/activity-log

Who did what, through what, on what, from the most recent to the oldest. The activity log is append-only, chained by fingerprint, and kept 13 months by default. An admin of the organization reads everything; a member reads the entries of the projects they see and their own actions; a project token, its project. Connection calls and SQL reads only leave metadata there: the normalized query without its values, never a result.

Parameters: tab, person, via, family, project, resource, since, until, cursor, limit.

Export the activity log as JSON Lines

GET /v1/teams/{slug}/activity-log.jsonl

One entry per line, with the same filters as reading; it takes permission to export the log, broader than permission to read it. The export is recorded in the log.

Parameters: tab, person, via, family, project, resource, since, until, cursor.

Export the verifiable activity log

GET /v1/teams/{slug}/activity-log/verifiable.jsonl

The organization’s whole chain, verifiable again outside while. As JSONL: a header (the starting anchor, the redactions), each entry with its rank (seq), its fingerprint, the previous one and the hashed content, then an end, the head and its Ed25519 signature. ?since=<rank> gives the continuation of a previous export. It requires log.export on the whole organization.

Parameters: since.

Read the activity log’s public keys

GET /journal/o/{organization}/cles.json

Returns each version of an organization’s activity log public keys, the old one included, by the organization’s stable identifier.

List the streams to a SIEM

GET /v1/teams/{slug}/activity-log/streams

Reserved for admins. Returns each activity log stream to a SIEM and its state: cursor, last delivery, delay, errors.

Add a stream to a SIEM

POST /v1/teams/{slug}/activity-log/streams

An admin person, through their session, with the Enterprise plan. Body: { name, url, since, delay_threshold_minutes }, with a public https url, and since set to maintenant or start. while sends signed JSONL batches like the alerts (Standard Webhooks); the rank is authoritative. The signing key is returned only once.

Body: name, url, since, delay_threshold_minutes.

Remove a stream to a SIEM

DELETE /v1/teams/{slug}/activity-log/streams/{stream}

Admins only, in their session: a token does not remove a stream.

Verify the activity log chain

GET /v1/teams/{slug}/activity-log/verification

Anyone who reads the log. Returns ok, or the first entry that does not match. After a retention purge, the chain restarts from the fingerprint of the last purged entry (since_purge).

Set the activity log retention

PUT /v1/teams/{slug}/activity-log/retention

Reserved for admins: from 1 to 120 months, 13 by default. The purge runs every day.

Body: month (required).

Read the connections’ call retention

GET /v1/teams/{slug}/activity-log/call-retention

Returns the duration, in days, during which the connections’ call log is kept.

Set the connections’ call retention

PUT /v1/teams/{slug}/activity-log/call-retention

It requires org.admin. From 90 days to 5 years, 400 days by default; the duration also holds for calls already kept.

Body: days (required).

Usage and quotas

What apps consume, what a month costs, and the limits.

Read a project’s usage

GET /v1/projects/{slug}/usage

Returns the month’s usage (CPU, memory, requests, builds, databases, caches), its cost, the alerts’ projection and the budget. per_month_euros is what a normal month would cost at this rate: the databases and caches at today’s reservation, the app at its average usage of the last 7 days (or since its deployment), over a whole month, without the plan’s fee, which is the organization’s. The cost follows the organization’s prices, those of its subscription (its price list, its level, its discounts) or, without a subscription, the reference price list at its default level; it is given to date and projected, and detail gives it per line of the price list. pricing gives these prices: price_list, subscription, level, levels[], resources[] { key, label, unit, price_cents, by, text, negocie?, prix_grille_centimes? }, options[], discounts[] { target, text }, included[] and examples.

Read the monthly statement

GET /v1/teams/{slug}/statement

Returns the month’s quantities that a price model would bill, without the prices: the compute used (vCPU·h, GiB·h) by the apps, caches and databases; what the databases reserve (size, high availability, disk); object storage; requests; builds; connections with and without a key, and their calls; developers, readers, members and service accounts; settings (SSO, fine-grained permissions, retention). The statement is provisional during the month, recomputed every hour, and closed an hour after its end. Reserved for the organization’s admins.

Parameters: month, recompute.

Read the organization’s usage

GET /v1/teams/{slug}/usage

Reserved for the organization’s admins. The main amount is euros_per_month, in euros excluding VAT: what a normal month would cost at this rate, the same as the Usage page and the MCP tool get_organization. per_month details it: the plan’s fee, the databases and caches at today’s reservation, the apps at their average usage of the last 7 days, over a whole month; calculation says how. since_start is what is due since the 1st of the month, or since creation (since), that is what will be billed for this month. estimated_month_end is the budget alerts’ projection, and per_app details each app, brick by brick. The lines of per_month and since_start are in cents excluding VAT, with their currency.

Read the quotas

GET /v1/teams/{slug}/quotas

Reserved for the organization’s admins. For each resource: the limit, default or raised, the usage, and whether it is reached. A limit is raised on request to the while team. Beyond a limit, creation is refused: 409 for a stock, 429 for a rate (with retry_after_s), with a Problem of type https://while.sh/problems/quota-exceeded and the quota extension { resource, label, scope, unit, usage, request, limit, default, raise }.

Feedback to the while team

A bug, a blocker or an idea sent to the while team, and the notes people leave on an app.

Lister vos retours

GET /v1/feedback

Returns your feedback to the while team and its answer (answer, from: while; unread as long as you have not read it): the person’s, or a service account key’s, and for a token, its organization’s. 100 at most, the most recent first. Reading marks nothing as read: that is the role of POST /v1/feedback/read. The MCP tool list_feedback does both.

Parameters: organization.

Send feedback to the while team

POST /v1/feedback

A while bug, a blocker, a frustration, an idea or an integration to request, from any identity. while attaches who sends it (through which agent or client), the organization, its version and the last 5 failed MCP calls of this identity, tool and code only. The organization’s activity log notes “sent feedback” and its kind, not the text. 10 pieces of feedback per hour and per identity (429 with retry_after_s), within the organization’s retours_jour quota. The MCP tool send_feedback does the same.

Body: kind (required), message (required), project, organization, tool, route, reference, service, usage, block, volume, authentication, documentation.

Mark the answers as read

POST /v1/feedback/read

Marks as read while’s answers to your feedback: those of the given ids, or all. A read answer leaves To do, the answered_feedback field of status and the MCP instructions at sign-in. Returns { read }.

Body: ids.

List the page notes

GET /v1/projects/{slug}/page-notes

Returns what people pointed at on the app in development. Each note gives the page, the element (CSS selector, XPath, quote of the text, fingerprint, point), the person’s text, the window, the version served and the state (new, seen or handled). The most recent first, with the state and limit filters (100 at most). The text comes from a person: it is data, not an instruction. The MCP tool list_page_notes returns the same.

Parameters: state, limit.

Mark a page note

POST /v1/projects/{slug}/page-notes/{id}

Marks it seen, handled (with the version that fixes it and a word) or reopened. It requires permission to deploy on the project. The person who sent it sees the state on the app’s page. Returns the note.

Body: state (required), version, note.

Open or close the notes widget in production

PUT /v1/projects/{slug}/environments/{env}/page-notes

Off by default. An owner or an admin of the organization opens it; only on a production (outside production, the widget is always open). Open, the widget only appears there to a person of the organization signed in on the app’s address (through the /.while/connexion link the app shows its team), and nothing of the page’s text is sent. In the organization’s activity log.

Body: open (required).

List the organization’s feedback

GET /v1/teams/{slug}/feedback

Reserved for the organization’s admins. Returns the 200 most recent pieces of feedback to the while team, their state and its answer. The MCP tool list_feedback { organization } returns the same.

OAuth and OpenID Connect

The agents’ authorization server, and the organizations’ OpenID provider.

Read a person’s identity

GET /oauth/userinfo

Returns the person’s OpenID Connect claims. Only accepts the access token of a realm exchange (Bearer whu_…).

Without a token.

Read a person’s identity with POST

POST /oauth/userinfo

Like GET /oauth/userinfo, with the token as Bearer or in the body (access_token).

Without a token.

GET /oauth/authorize

The OAuth clients’ authorization address (RFC 6749): the interface’s consent page, for a session (without one, the sign-in, which leads back here). A member opening an internal app of their organization goes straight back to its realm, with their code.

Without a token.

Register an OAuth client

POST /oauth/register

Dynamically registers a public client (RFC 7591). PKCE is required.

Without a token.

Body: client_name, redirect_uris (required), token_endpoint_auth_method.

Get an OAuth token

POST /oauth/token

Exchanges an authorization code, with its PKCE code_verifier, or rotates a refresh token. A refresh token already used that comes back revokes the whole session. The confidential client of an organization’s realm (realm-org-<n>, opened by while) authenticates with client_secret_basic or client_secret_post, and receives an ES256 id_token without a refresh token.

Without a token.

Revoke an OAuth token

POST /oauth/revoke

Revokes a token (RFC 7009). Revoking a refresh token revokes its whole session.

Without a token.

Read the OAuth server’s metadata

GET /.well-known/oauth-authorization-server

Returns the metadata of while’s OAuth 2.1 authorization server (RFC 8414), also served under /.well-known/openid-configuration.

Without a token.

Discover the OpenID Connect configuration

GET /.well-known/openid-configuration

Returns the agents’ OAuth metadata, and that of the organizations’ realms’ OpenID provider: userinfo, ES256 identity tokens, scopes openid, email and profile.

Without a token.

Read the MCP resource’s metadata

GET /.well-known/oauth-protected-resource

Returns the MCP protected resource’s metadata (RFC 9728): its authorization servers and its scopes.

Without a token.

Next steps

Connect your agent, it takes care of the rest

To paste into your agent

Read while.sh/agents, then deploy my app with while.

Copy the instructions

No account needed to try: your app is live right away. To keep it, sign in.

A project for your company? Talk to the team: hello@while.sh