HTTP API
Base URL https://api.froogcorp.com/v1. Authenticate with an API key from the dashboard as a bearer token. Keys expire after 90 days by default (choose up to a year, or never, when you create one), and an expired key gets 401 api_key_expired. Keys can be scoped to actions and sandboxes. Requests and responses are JSON, and errors look like {"error": {"code": "...", "message": "...", "request_id": "req_..."}}. Every response has an X-Request-Id header with the same id. Quote it when you report a problem.
Rate limit. Each account can send 10,000 requests per 60 seconds, to any endpoint: all its API keys, in-sandbox keys and dashboard sessions together, from any address. Requests without a valid key count per IP address instead (each /64 over IPv6), 10,000 per 60 seconds. Sandboxes on one froog host share its IPv4 address, so their requests without a key aren't counted together, and over IPv6 each sandbox counts by its own address. A request counts once when it arrives, so a long-running one counts once. Every response carries RateLimit-Limit, RateLimit-Remaining, RateLimit-Reset (seconds until the window ends) and RateLimit-Policy (10000;w=60). Past the limit you get 429 account_rate_limited (or 429 ip_rate_limited without a key) with Retry-After in seconds.
$ curl -s https://api.froogcorp.com/v1/sandboxes \
-H "Authorization: Bearer $FROOG_API_KEY" \
-H "Content-Type: application/json" \
-d '{"name": "ci-1", "size": "small"}'
$ curl -s https://api.froogcorp.com/v1/sandboxes/ci-1/exec \
-H "Authorization: Bearer $FROOG_API_KEY" \
-H "Content-Type: application/json" \
-d '{"command": "uname -a", "timeout_secs": 60}'
{"exit_code":0,"stdout":"Linux froog-… 6.8.0 …\n","stderr":""}
Creating, starting, forking and restoring fail with 402 subscription_required without a plan, 402 insufficient_funds when this period's plan usage and the wallet are used up, and 429 trial_compute_limit_reached past the free trial's 25 hours. During the trial, no auto-stop or a timer over 2 hours fails with 403 trial_auto_stop_required, and an account whose card already had a trial elsewhere gets 402 trial_blocked. xlarge below the $100 plan fails with 403 plan_required, a paused plan with 402 plan_paused, and a closed account with 403 account_closed. In an organization, a member past their caps gets 429 member_limit_reached or 402 team_member_cap_reached. Past a start limit (per minute, hour or day, see GET /v1/limits) without extra starts they fail with 429 limit_reached and the headers Retry-After (seconds until a start is free again), RateLimit-Limit, RateLimit-Remaining, RateLimit-Reset and RateLimit-Policy (e.g. 10;w=60). Creating, stopping, starting, forking and restoring return 202 Accepted and continue in the background. Poll GET /v1/sandboxes/{name} until state is running or stopped.
Retries. Send an Idempotency-Key header (any unique string up to 255 characters) with POST /sandboxes and POST /sandboxes/{name}/fork to make them safe to retry after a timeout or a dropped connection. A retry with the same key within 24 hours returns the first response, with the same sandboxes and status and an Idempotent-Replayed: true header, instead of creating more. The same key with a different request gets 422 idempotency_key_reused, and a retry while the first request is still running gets 409 idempotency_key_in_use. Errors aren't kept, so a retry after one runs again. Keys belong to the account. froog new and froog fork send one and retry with it when the connection drops.
Streamed exec. With "stream": true (or Accept: application/x-ndjson), POST /sandboxes/{name}/exec answers application/x-ndjson: one JSON object per line, as the output arrives. First {"type":"started","command_id":…}, then {"type":"stdout","data":…} and {"type":"stderr","data":…} chunks (UTF-8 text), then exactly one {"type":"exit","exit_code":0,"timed_out":false}, or {"type":"error","code":…,"message":…} if the connection to the sandbox broke. Errors before the command starts (not running, bad request) are ordinary JSON errors. Hanging up stops the command. It's in the history with mode stream. froog exec streams this way, and the SDKs have execStream (exec_stream in Python).
Watching files and ports. Two more NDJSON streams stay open while you listen (up to timeout_secs, 1-3600, default 3600, then reconnect). GET /sandboxes/{name}/files/watch?path=src&recursive=true sends {"type":"ready","method":"inotify"}, then {"type":"change","event":"create"|"modify"|"delete","path":…,"dir":…} for each change under the path (a file written and closed is modify, a move is a delete and a create), then {"type":"end","reason":"timeout"}. Paths come back as given: relative to ~ or absolute. It uses inotify where the image has inotify-tools, and otherwise scans every second ("method":"poll"), which sees changes up to a second late. A path that doesn't exist sends {"type":"error"}. GET /sandboxes/{name}/ports/watch sends ready, then {"type":"open"|"close","port":3000,"protocol":"tcp"} as TCP ports start or stop listening on any address (the ones already listening come first as open), read every second. Both need a running sandbox (or start an auto-start one), file.read and sandbox.read respectively, and each holds one of the sandbox's 8 command slots while open. SDKs: sb.files.watch(path) yields the changes. The flat watchFiles and watchPorts yield every event.
$ curl -sN https://api.froogcorp.com/v1/sandboxes/ci-1/exec \
-H "Authorization: Bearer $FROOG_API_KEY" -H "Content-Type: application/json" \
-d '{"command": "make test", "stream": true}'
{"type":"started","command_id":"cmd_…"}
{"type":"stdout","data":"ok build\n"}
{"type":"exit","exit_code":0,"timed_out":false}
$ KEY=$(uuidgen) # reuse it for every retry of this request
$ curl -s --retry 3 https://api.froogcorp.com/v1/sandboxes \
-H "Authorization: Bearer $FROOG_API_KEY" \
-H "Idempotency-Key: $KEY" \
-H "Content-Type: application/json" \
-d '{"name": "ci-2"}'
| GET | /sandboxes?label=&name=&name_prefix=&state=&sort=&limit=&cursor= | List sandboxes, oldest first (sort=desc: newest first). Filters: label=KEY=VALUE or label=KEY (repeat to require several), name, name_prefix, state (comma-separated for several). Without limit you get every match. With limit (1-200), an X-Next-Cursor header comes back while more follow: pass it as cursor. The cursor is <created_at>_<id> of the page's last sandbox. Unknown parameters get 400 invalid_query |
| POST | /sandboxes | Create: {name?, size?, ttl_secs?, count?, setup?, environment?, os?, from?, snapshots?, monitoring?, env?, files?, labels?, max_lifetime_secs?, delete_after_secs?, expires_in_secs?, repo?, auto_start?}. ttl_secs is the auto-stop timer: seconds after each start (default 3600, 0 = none, at most 30 days). auto_start: true starts it on a URL request or an API call while stopped (see auto-start). max_lifetime_secs stops it that long after each start and delete_after_secs deletes it after staying stopped that long (see lifetime). repo: {url, branch?, dir?} is cloned into it at boot (see one sandbox's repo). labels is a {KEY: VALUE} object (see labels). setup is a script run once after the first boot, environment: false starts without your environment (the default from a share code or its copy, true opts in), from starts from a saved snapshot (or a share code fsh_…), environment_name picks an environment (default: your default one), files: [{path, content, encoding?}] are written into ~ before it's running (encoding: "base64" for binary, up to 20, 1 MiB together) |
| GET | /sandboxes/{name} | Show one. Lists and this include period_cost_usd (its running time this billing period, at list prices), setup_status (none, running, done, failed) and setup_error, max_lifetime_secs and stop_at, delete_after_secs and delete_at, expires_at, repo and auto_start |
| GET | /sandboxes/{name}/metrics | Read now from a running sandbox: {sampled_at, cpu_count, cpu_percent, memory_used_bytes, memory_total_bytes, disk_used_bytes, disk_total_bytes, network_rx_bytes, network_tx_bytes, io_pressure} |
| GET | /sandboxes/{name}/usage | Its running time and cost: {name, from, to, seconds, cost_usd, lines: [{size, seconds, cost_usd}]}. ?since=&until= in unix seconds (default: this billing period so far) |
| PATCH | /sandboxes/{name} | Rename, change the auto-stop timer (a running sandbox then stops that long from now), labels, lifetime, auto-delete or auto-start: {name?, ttl_secs?, labels?, max_lifetime_secs?, delete_after_secs?, expires_in_secs?, auto_start?} (0 removes a lifetime or auto-delete). In labels, a value sets that label and null removes it. Labels you don't name stay |
| GET | /saved-snapshots | Your saved snapshots: [{name, id, source, source_deleted, os, size, size_gb, status, created_at}]. Start one with POST /sandboxes {"from": NAME}. GET /limits has saved_snapshot_allowance: {included, used, extra, price_per_extra_day_usd} |
| GET · DELETE | /saved-snapshots/{name} | One saved snapshot (poll status after saving, until available) · remove it (and revoke its share codes) |
| GET · POST | /saved-snapshots/{name}/shares | Its share codes: [{id, name, created_at, revoked_at, uses, last_used_at}] · make one: {id, name, code, created_at} (the code only here) |
| DELETE | /saved-snapshots/{name}/shares/{id} | Revoke a share code |
| GET | /saved-snapshots/{name}/files · …/files/download | Inside a saved snapshot: ?path=P (default ~): a file's details or a folder's entries · a file's bytes or a folder as tar |
| GET | /snapshots?sort=&limit=&cursor= | Every snapshot of the account, newest first (sort=asc: oldest first), each with its sandbox. Paged like /sandboxes: with limit (1-200), X-Next-Cursor comes back while more follow |
| DELETE | /sandboxes/{name} | Delete it and its automatic snapshots (saved snapshots stay) |
| POST | /sandboxes/{name}/exec | Run a shell command: {command, timeout_secs?, cwd?, env?, stream?, stdin?, stdin_b64?} (max 3600) → {exit_code, stdout, stderr, timed_out, command_id}. cwd is relative to ~ or absolute, env a {KEY: VALUE} object for this command only. Past its timeout the command is killed: timed_out: true, exit code 124, and the output until then. stream: true (or Accept: application/x-ndjson): see streamed exec. stdin (text) or stdin_b64 is the command's input, up to 1 MiB (see standard input) |
| POST | /sandboxes/{name}/wait | Wait until something listens on a TCP port inside it: {port, timeout_secs?} (1-600, default 60) → {port, ready, waited_ms} (see wait for a port) |
| GET · POST | /sandboxes/{name}/urls | List · add (or change) a URL: {port, public?, subdomain?, title?} (up to 50 per sandbox), private (with ?_token=) unless public: true |
| DELETE | /sandboxes/{name}/urls/{port} | Remove it |
| GET · POST | /sandboxes/{name}/urls/{port}/tokens | Live viewer tokens of a private URL · make one: {ttl_secs?, label?} → {id, token, url, label, created_at, expires_at} (see a token per viewer) |
| DELETE | /sandboxes/{name}/urls/{port}/tokens/{id} | Revoke a viewer token |
| GET | /sandboxes/{name}/files?path=P | A file's details, or a folder and its entries. Relative paths are relative to /home/user |
| GET · PUT | /sandboxes/{name}/files/content?path=P | Download · upload raw bytes, up to 100 MiB (parent folders are created, and &mode=755 sets permissions) |
| PUT | /sandboxes/{name}/files/archive?path=DIR | Unpack a .tar.gz (raw bytes, up to 100 MiB) into a folder, created if missing. froog push uses it |
| DELETE | /sandboxes/{name}/files?path=P | Delete a file or empty folder, or with &recursive=true a folder and its contents |
| POST | /sandboxes/{name}/files/move · /files/mkdir | {from, to} · {path, mode?} (parents are created, mode is octal, e.g. "700", for the folder itself) |
| POST | /sandboxes/{name}/files/permissions | chmod: {path, mode, recursive?}, mode octal like "755". The owner stays the sandbox user |
| POST | /sandboxes/{name}/files/download-url · /files/upload-url | {path, expires_in?} → {url, method, expires_at}: a link that GETs the file, or takes a PUT of its raw bytes, with no Authorization header (15 minutes by default, 60 s to 24 h, up to 100 MiB). Hand it to a browser, a CI step or another service |
| GET | /sandboxes/{name}/files/search?pattern=*.py&path=~ | Paths whose name matches a glob: {files, truncated, errors?} (at most 10,000, absolute) |
| GET | /sandboxes/{name}/files/find?pattern=TEXT&path=~®ex=false | Every line containing the text (or an extended regex): {matches: [{file, line, content}], truncated}. At most 1,000, lines cut at 500 characters, binary files skipped, symlinks not followed |
| GET | /sandboxes/{name}/files/watch?path=&recursive=&timeout_secs= | Stream file changes under a path as NDJSON (see watching) |
| GET | /sandboxes/{name}/ports/watch?timeout_secs= | Stream TCP ports starting or stopping to listen as NDJSON |
| POST | /sandboxes/{name}/files/replace | {paths, pattern, new_value}: replace every occurrence of a plain text, byte for byte, in up to 100 files (5 MiB each), with one {file, success, replacements?, error?} per file |
| GET · POST | /sandboxes/{name}/jobs | List · start a background command: {command, cwd?, env?, stdin?, stdin_b64?, stdin_open?} |
| GET | /sandboxes/{name}/jobs/{id}?offset=N | State, exit code and output from byte offset. Pass next_offset back to follow |
| POST | /sandboxes/{name}/jobs/{id}/stop | Stop it (TERM, then KILL) |
| POST | /sandboxes/{name}/jobs/{id}/stdin | Write to the input of a job started with stdin_open, and/or close it: {data?, data_b64?, close?} → 204 (see writing to a running job) |
| GET | /sandboxes/{name}/command-history?mode=&sort=&limit=&cursor= | Commands, newest first (sort=asc: oldest first). Pass next_cursor back as cursor for older ones |
| GET | /sandboxes/{name}/command-history/{id} | One record: text, mode, status, exit code, output sizes |
| GET | /sandboxes/{name}/command-history/{id}/logs?stream=stdout|stderr&offset=N&encoding=base64 | Stored output from offset (≤1 MiB): {data, next_offset, size, done} |
| GET | /sandboxes/{name}/processes | Processes, busiest first. A stopped sandbox answers with its last sample and live: false |
| GET | /sandboxes/{name}/processes/samples | The 12 busiest processes every 30 seconds, kept 7 days, newest first: ?since=&until= (unix seconds), limit (1-1000, default 120) |
| GET | /sandboxes/{name}/processes/records | Processes seen in the 30-second checks, kept 72 hours: ?since=&until=&running=true&limit= (1-1000, default 200) |
| GET | /sandboxes/{name}/metrics/history | CPU, memory and disk every 30 seconds, kept 7 days: ?since=&until=&step= |
| GET · PATCH | /account/alerts | Which emails the account gets, and automatic rebuilds: {emails: {KIND: bool}, auto_rebuild} |
| POST | /sandboxes/{name}/prompt | Prompt an agent and wait: {prompt, agent?, model?, effort?, fast?, new?, conversation_id?, attachments?, timeout_secs?}, or 400 provider_not_configured without a key or login |
| POST | /sandboxes/{name}/prompts | Queue a prompt (same body). 202 with the prompt. GET lists them (?conversation=) |
| GET | /sandboxes/{name}/prompts/{id} | Status, result, error. /events: its events (JSON, SSE or NDJSON) |
| GET | /sandboxes/{name}/events | Every conversation's events (?conversation=, ?cursor=) |
| GET | /sandboxes/{name}/conversations[/{id}] | Conversations, or one with its turns. DELETE removes one |
| POST | /sandboxes/{name}/steer, /interrupt | {message, conversation_id?}. {conversation_id?} |
| GET | /provider-models | Harnesses, models, effort levels, fast mode (no key needed) |
| GET | /subscriptions | Claude and ChatGPT sign-ins. POST /subscriptions/{provider}/login starts one |
| POST | /sandboxes/{name}/stop | Run ~/.froog/pre-stop, snapshot and stop: {force?} (true keeps the last good snapshot if this one fails). The answer's stop_id tracks it (stopping again while it's pending answers the same id) |
| GET | /stops/{id} | A stop: {id, sandbox, status, requested_at, ended_at, error}, status pending, failing (its snapshot failed: the sandbox keeps running, unbilled, and froog retries this stop, see Stopping safely), completed (stopped, or erased with snapshots off), superseded (a start cancelled a failing stop) or failed (the sandbox is in error, a start retries from its last snapshot). Auto-stops and billing stops get one too |
| POST | /sandboxes/{name}/start | Start from the latest snapshot: {size?, ttl_secs?, environment?, env?, environment_name?}. environment_name moves it to that environment's latest version. ttl_secs is a new auto-stop timer, counted from this start (0 = none), environment: false boots without your environment (true with it again), env adds to its own variables. All are kept for later starts. New variables or environment need a fresh boot, so such a start doesn't resume warm |
| POST | /sandboxes/{name}/fork | {name, snapshot?, count?, size?, env?, ttl_secs?, snapshots?, environment?, environment_name?} (env goes on top of the source's own variables, ttl_secs defaults to the source's, snapshots: false makes copies a stop erases, environment: false runs them without your environment, your logins removed, environment_name puts them on that environment's latest version instead of the source's) |
| POST | /sandboxes/{name}/restore | {snapshot} (id or label) |
| GET | /sandboxes/{name}/snapshots?sort=&limit=&cursor= | List snapshots, oldest first (sort=desc: newest first), paged like /snapshots |
| POST | /sandboxes/{name}/snapshots | Save one: {label?}. With a new label past your plan's included saved snapshots and an empty wallet: 402 credit_required |
| DELETE | /sandboxes/{name}/snapshots | Delete every automatic snapshot: {deleted, kept}. Saved snapshots stay, and so does the one a stopped sandbox starts from (kept) |
| GET | /sandboxes/{name}/snapshots/{snap} | One snapshot by id or label. latest is the newest available one |
| DELETE | /sandboxes/{name}/snapshots/{snap} | Delete a snapshot |
| GET | /sandboxes/{name}/snapshots/{snap}/files · …/files/download | Inside a snapshot (the sandbox may be stopped): ?path=P (default ~): a file's details or a folder's entries · a file's bytes or a folder as tar (X-Froog-Entry-Kind: file|dir) |
| POST | /sandboxes/{name}/desktop | A link to the desktop in froog's viewer: {url, expires_at} (open within 10 minutes). ?theme=dark|light |
| POST | /sandboxes/{name}/browser | A link to the browser view (only Chrome). Body {"profile": "/abs/dir"} (optional), and ?theme=dark|light picks the theme |
| GET | /sandboxes/{name}/ssh | SSH host, user, port and pinned host key |
| POST | /sandboxes/{name}/touch | Record activity (last_active_at). It doesn't move the auto-stop deadline: PATCH ttl_secs does |
| GET · POST | /inboxes | List · make an email inbox, $2 a month {username?, name?, sandbox?} (Tokenmail) |
| GET · PATCH · DELETE | /inboxes/{inbox} | Show · rename or attach to a sandbox {name?, sandbox?} · delete (by address, the part before @, or id) |
| GET · POST | /inboxes/billing · /inboxes/billing/checkout | Seats, inboxes and the subscription · a Checkout url that buys inboxes {quantity?} |
| GET | /inboxes/{inbox}/messages?since=&limit= | Received messages, newest first, with codes and links |
| GET · DELETE | /inboxes/{inbox}/messages/{id} | One message with text and html · delete; …/raw as received |
| GET | /inboxes/{inbox}/wait?since=&after=&sender=&subject=&timeout= | Wait for the next matching message (up to 120 s): {message}, null on timeout |
| GET · POST | /webhooks | List · add {url, events?, name?} (returns the signing secret once) |
| GET · PATCH · DELETE | /webhooks/{id} | Show · change {url?, events?, name?, enabled?} (the secret stays, "" removes the name) · remove |
| GET | /webhooks/{id}/deliveries?limit= | Delivery attempts of the last 30 days, newest first: [{event_id, event, sandbox?, attempt, status, response_code?, error?, duration_ms, created_at, next_attempt_at?}]. limit 1-200, default 50 |
| POST | /webhooks/{id}/rotate · /test | New secret · send a ping |
| GET · DELETE | /account/connections · /account/connections/{id} | Sign-in connections · remove one (not the last: 409 last_connection) |
| POST | /account/connections/email · /email/verify | Send a code to {email} (202) · add it with {email, code} |
| PUT | /account/primary | {connection}: its address gets the account's emails |
| GET · POST | /github · /github/connect | GitHub connection · a link that installs the app |
| GET · PUT · DELETE | /github/repos · /github/token-repos · /github/installations/{id} | Repositories the app sees · limit the sandbox token {repos} · unlink |
| POST | /sandboxes/{name}/github-token | A one-hour GitHub token for the sandbox {owner?}: {token, expires_at, owner, repositories} |
| GET · PUT | /ci · /ci/settings | Pushbox overview ?repo=&state=&limit= · public repositories {public_repos, public_users} |
| GET | /ci/runs · /ci/runs/{run_id} | Workflow runs, a page at a time ?workflow=&repo=&branch=&state=&q=&limit=&offset= · one run and its jobs |
| GET | /ci/jobs/{id} · /ci/jobs/{id}/logs | One job · its log (text) |
| GET | /env | Your environment variables (names and masked previews) |
| PUT · DELETE | /env/{name} | Set {value} · remove a variable |
| GET · PATCH | /environment | The default environment: {name, default, version, repos, setup, repo_setup, files, updated_at} (file names and sizes) · change {repos?, setup?, repo_setup?} (setup: "" removes it, repo_setup: {"app": "npm ci"}, "" removes one). A change makes a new version |
| PUT · DELETE | /environment/files | Add or replace {path, content} · remove ?path=P |
| GET · POST | /environments | Your environments: [{name, default, version, repos, files, setup, created_at, updated_at, sandboxes: [{version, sandboxes}]}] · create {name, copy_from?, default?} |
| GET · PATCH · DELETE | /environments/{env} | One environment, like /environment · change {repos?, setup?, repo_setup?, name?, default?} (a rename or a new default makes no version) · delete it (not the default) |
| PUT · DELETE | /environments/{env}/files | Add or replace {path, content} · remove ?path=P |
| GET | /environments/{env}/versions | [{version, created_at, change, sandboxes}], newest first |
| POST | /environments/{env}/upgrade | Move its sandboxes to the latest version: {sandboxes?} (names, default all) → {environment, version, applied, at_next_start} |
| GET · POST | /ssh-keys | List · add {name, public_key} |
| DELETE | /ssh-keys/{id} | Remove a key |
| GET | /account | Your account and limits |
| GET · POST | /api-keys | List · create {label?, ttl_days?, never_expires?, preset? | actions?, sandboxes?, labels?, environments?}. labels limits it to sandboxes with all of them (see label-limited keys), environments (names) to sandboxes of those (see environment-limited keys). Keys expire after 90 days unless you choose otherwise (up to 365, or never). Without a scope the key has full access. The key is returned once |
| DELETE | /api-keys/{id} | Revoke a key |
| GET · POST | /orgs | Your personal account and organizations · create one {name} |
| PATCH | /orgs/active | The active organization {org} (id, name or personal) |
| GET · PATCH · DELETE | /orgs/{org} | One organization · {name?, default_visibility?} · delete it |
| GET · PATCH · DELETE | /orgs/{org}/members · /members/{member} | Members and invites · {role?, max_running_sandboxes?, max_usage_usd?} · remove one |
| POST · DELETE | /orgs/{org}/invites · /invites/{id} | Invite {email, role?} · revoke |
| POST · PUT | /orgs/{org}/leave · /transfer · /seats | Leave · {member} becomes the owner · {seats} |
| GET · POST | /org-invites/{token} · /org-invites/accept | What an invite is for · accept it {token} |
| POST | /sandboxes/{name}/share · /unshare | Shared with the organization · private to its creator |
| POST | /api-keys/{id}/rotate | A new secret for the key (returned once), with the same id, scope and expiry |
| GET | /audit?action=&sandbox=&since=&until=&sort=&limit=&cursor= | The audit log, newest first: {events, next_cursor} (needs account.admin) |
| GET | /audit/export?format=csv|ndjson | Every matching entry of the 90 days as one file, oldest first (same filters) |
| GET · PATCH | /account/data-retention | {zero_data_retention, snapshots_off, monitoring_off}. Changing needs a dashboard session, and turning on zero data retention needs confirmation: "delete stopped sandbox data" (the answer adds deletion_operation) |
| POST | /account/close | Close the account, with the header X-Froog-Confirm-Close: EMAIL: 202 {account, operation}. A full-access key or a session |
| POST | /account/reopen | Reopen it within 30 days: {account, operation} |
| GET | /deletion-operations/{id} | {id, kind, status, stage, created_at, updated_at, expected_by, error} |
| POST | /dashboard-link | {url, expires_at}: opens the dashboard signed in, once, within 15 minutes, after a "Continue as EMAIL?" page (opening the link alone signs nobody in). A full-access key or a session |
| GET · PUT · DELETE | /telemetry | The OpenTelemetry endpoint: PUT {endpoint, headers} answers {telemetry, test: {ok, status, message}} |
| POST | /feedback | Send feedback: {message} |
| GET | /usage | This billing period: {lines: [{size, seconds, cost_usd}], saved_snapshot_days, snapshot_cost_usd, total_usd, included_usd, billable_usd, from, to, free_trial_until}. billable_usd is the part past the plan, paid from the wallet. ?month=YYYY-MM: that calendar month at list prices |
| GET | /billing | {plan, plans, rates, included_usd, used_usd, remaining_usd, wallet_usd, auto_topup_usd, auto_topup_failures, min_topup_usd, grace_until, wallet_bonus, extra_starts, keep_disks_until}. plan is {usd, status, period_start, period_end, trial_ends_at, cancel_at_period_end, max_sandboxes, saved_snapshots}, and status is trialing, active, past_due (a renewal's payment is being retried; the plan still works), unpaid (the first payment failed; the plan doesn't work until it's paid)… |
| POST · DELETE | /billing/plan | Choose or change a plan {plan}, answers {url?, billing} with a Stripe Checkout url for the first plan · cancel at the end of the period (?keep_disks=true: pause) |
| PUT | /billing/extra-starts | {enabled}: extra starts on or off |
| GET | /billing/wallet/quote?amount_usd=N | {amount_usd, bonus_percent, bonus_usd, credited_usd} |
| POST | /billing/trial/end | End the free trial now and start paying for the plan |
| POST | /billing/wallet/pay | Add {amount_usd} to the wallet, charged to the saved card |
| PUT | /billing/wallet/auto-topup | Auto top-up amount {amount_usd}, 0 turns it off |
| POST | /billing/portal | A Stripe page URL to change the card or download invoices |