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.
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.
Send sources through a signed link
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.
Download sources through a signed link
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.
Advertise git references
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.
Read the GitHub or GitLab link
GET /v1/projects/{slug}/forge
Returns the repository linked to the project, without its token.
Link a GitHub or GitLab repository
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.
Domains and access links
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.
List the access links
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.
Create an access link
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.
Revoke an access link
DELETE /v1/projects/{slug}/environments/{env}/links/{id}
The link stops working right away. You need to be a person, with project.configure.
Create a presigned link to an object
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.
Read a link to the company sign-in
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).
Link your account to the company sign-in
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.
Link a group to a company group
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.
Link a group to an SSO group
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.
Open the consent page
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.