Reference
API and schema
v1 contracts for heartbeats, performance telemetry, logs, events, suite runs, and authenticated fleet reads.
Reviewed 2026-09-12
The API separates ingest from reads. Ingest endpoints use HMAC; read endpoints accept an administrator session or a valid Bearer token.
Endpoints
| Method | Path | Auth | Purpose |
|---|---|---|---|
POST |
/api/ingest |
HMAC | Agent heartbeat. |
POST |
/api/ingest/telemetry |
HMAC | Bounded Monitor performance window. |
POST |
/api/ingest/logs/plans |
HMAC | Reserve one bounded log chunk and obtain an upload grant. |
POST |
/api/ingest/logs/commits |
HMAC | Confirm one uploaded log chunk through file.cheap. |
POST |
/api/ingest/logs/finalize |
HMAC | Finalize one bounded log session after all chunks commit. |
POST |
/api/ingest/issues |
HMAC | One container death from a box running observability.crashCapture: death facts plus up to three closed issue digests. Idempotent per death. |
POST |
/api/ingest/events |
HMAC | Batch of explicit events. |
POST |
/api/ingest/runs |
HMAC | Results and artifacts. Send x-chalupa-want-inserted: 1 to also receive insertedSourceRunIds, the runs this request stored for the first time. |
POST |
/api/ingest/runs/evidence |
HMAC | Append-only curated evidence for a reported run, idempotent by SHA-256. |
GET |
/api/fleet |
admin | Aggregated fleet view. |
GET |
/api/logs |
admin | Bounded log sessions, chunks, and redacted excerpts. |
POST |
/api/logs/downloads |
admin + Vercel OIDC | Revalidate one committed log chunk and redirect to an exact short-lived file.cheap download. |
GET |
/api/logs/lines |
admin | Decoded, redacted log lines for a time window, filtered by text, level and service. |
POST |
/api/artifacts/open |
admin + Vercel OIDC | Revalidate one stored run-evidence artifact and redirect to an exact short-lived download. |
POST |
/api/auth/login |
token in body | Creates an HttpOnly cookie. |
POST |
/api/auth/start |
none | Starts sign-in or account creation for an email. Always 200 with the same body, whether or not the address has an account. |
POST |
/api/auth/logout |
none | Clears the cookie if present. |
GET |
/api/health |
none | Minimal process liveness. |
Writes are disabled while Chalupa runs in demo mode.
POST /api/logs/downloads accepts JSON or an HTML form with exactly
environmentSlug, chunkId, and artifactId. These values are stable
identifiers, not download capabilities. Chalupa joins the chunk back to its
environment in Neon, requires the stored state to be committed, and verifies
file.cheap's response against the stored artifact reference and SHA-256 before
issuing a 303. Cookie-authenticated forms require exact same-origin browser
provenance. Chalupa also requires the signed target to match the exact immutable
Vercel private Blob path and refuses a grant that outlives artifact retention.
The response is no-store and no-referrer; the signed URL and complete log
bytes are never stored by Chalupa.
Ingest authentication
Required headers:
content-type: application/json
x-chalupa-timestamp: <epoch-seconds>
x-chalupa-nonce: <unique-value>
x-chalupa-signature: v1=<hmac-sha256-hex>
Canonical string:
v1
<METHOD>
<PATH>
<TIMESTAMP>
<NONCE>
<SHA256_HEX_OF_BODY>
The signature uses CHALUPA_INGEST_KEY, which must contain 32 to 1024 bytes
with no leading or trailing whitespace or line breaks. The timestamp allows a
limited window, and each nonce is claimed exactly once.
Heartbeat
{
"version": "1",
"environmentSlug": "demo-harbor",
"environmentName": "Demo Harbor",
"observedAt": "2026-07-23T18:20:00.000Z",
"deployment": {
"provider": "digitalocean",
"providerId": "10001",
"region": "nyc3",
"sizeSlug": "s-2vcpu-4gb",
"launchedAt": "2026-07-23T17:50:00.000Z"
},
"ip": "192.0.2.44",
"uptimeSeconds": 1800,
"containers": [
{
"name": "demo-harbor-api-1",
"service": "api",
"role": "application",
"logs": "include",
"state": "running",
"memoryMiB": 312,
"cpuPercent": 4.8
}
],
"reaper": {
"idleChecks": 2,
"checkIntervalSeconds": 300,
"limitMinutes": 45
},
"disk": {
"mountedGiB": 20,
"usedGiB": 1.4
},
"runtime": {
"chalupaVersion": "0.17.0",
"manifestSha256": "0000000000000000000000000000000000000000000000000000000000000000",
"deliveredAt": "2026-07-23T17:52:00Z"
},
"monitor": {
"installed": true,
"binaryVersion": "2.2.0",
"forwarderActive": true,
"lastDeliveredAt": "2026-07-23T18:08:30Z"
},
"agentVersion": "1"
}
runtime is the stamp chalupa up or chalupa box refresh wrote on the box
(deliveredAt is when the box last verified it), and monitor is measured on
the box: the binary's own version, whether the forwarder is running, the last
window the control plane accepted, and lastRejectCode when it refused one.
Both are optional, so an agent that predates runtime delivery stays a valid
reporter; the console then shows nothing instead of guessing from the image.
service, role, logs, databaseKind, primary, and initializes are
optional, allowlisted metadata derived from the validated Compose plan. The
agent never forwards arbitrary labels, image references, environment values,
commands, container IDs, or hostnames. Older agents may send only the
container name and state.
Accepted response:
{
"accepted": true
}
The server can include additional counters or identifiers.
Performance telemetry
The adapter wraps one exact Monitor v1 window with the Chalupa deployment identity. This example is synthetic:
{
"version": "1",
"environmentSlug": "demo-harbor",
"deployment": {
"provider": "digitalocean",
"providerId": "10001"
},
"schema_version": 1,
"kind": "monitor.telemetry_window",
"session_id": "0123456789abcdef0123456789abcdef",
"sequence": 1,
"emitted_at": "2026-07-24T18:20:30.100Z",
"producer": {
"name": "monitor",
"version": "1.14.0"
},
"window": {
"from": "2026-07-24T18:20:00.000Z",
"to": "2026-07-24T18:20:30.000Z",
"sample_interval_ms": 5000,
"sample_count": 6,
"partial": false
},
"metrics": {
"system.cpu.usage": {
"unit": "percent",
"count": 6,
"min": 8.1,
"avg": 15.2,
"p95": 22.4,
"max": 22.4,
"last": 17.1
},
"system.memory.used": {
"unit": "bytes",
"count": 6,
"min": 536870912,
"avg": 541065216,
"p95": 545259520,
"max": 545259520,
"last": 543162368
}
},
"availability": {
"system.cpu.usage": {
"state": "observed",
"observed_samples": 6,
"missing_samples": 0
},
"system.memory.used": {
"state": "observed",
"observed_samples": 6,
"missing_samples": 0
},
"system.memory.available": {
"state": "unavailable",
"observed_samples": 0,
"missing_samples": 6
},
"system.memory.usage": {
"state": "unavailable",
"observed_samples": 0,
"missing_samples": 6
},
"system.memory.pressure": {
"state": "unavailable",
"observed_samples": 0,
"missing_samples": 6
},
"system.swap.used": {
"state": "unsupported",
"observed_samples": 0,
"missing_samples": 6
},
"system.network.receive_rate": {
"state": "unavailable",
"observed_samples": 0,
"missing_samples": 6
},
"system.network.transmit_rate": {
"state": "unavailable",
"observed_samples": 0,
"missing_samples": 6
},
"system.disk.read_rate": {
"state": "unavailable",
"observed_samples": 0,
"missing_samples": 6
},
"system.disk.write_rate": {
"state": "unavailable",
"observed_samples": 0,
"missing_samples": 6
},
"system.load.one_minute": {
"state": "unavailable",
"observed_samples": 0,
"missing_samples": 6
}
},
"alerts": [
{
"rule": "cpu_threshold",
"severity": "warning",
"count": 1
}
]
}
Every availability entry is required even when its metric is not observed. Metrics are optional only when their observed count is zero. The API rejects unknown fields, metrics, units, alert categories, non-finite numbers, invalid summary ordering, impossible counts, future timestamps, and bodies larger than 64 KiB.
session_id plus sequence is the producer retry identity within a
deployment. Repeating identical normalized content deduplicates; changing the
same identity returns telemetry_ingest_conflict. A window whose Monitor
producer version differs from the version pinned for its deployment returns
telemetry_monitor_version_mismatch. The API stores only the validated
projection, never the raw source line.
Source windows and their detailed anomaly rows are retained for 7 days and then atomically folded into indefinite hourly rollups before deletion. Each observed metric carries a bounded internal coverage duration derived from its observed sample count and cadence, capped by the signed source window. Hourly aggregation sums that duration independently of the hour-shaped display bucket, keeping rate integrations honest when data is sparse or partial. Deployment, session, hourly anomaly counts, and retry identity survive compute destruction and downsampling.
The private retention cron drains this work in bounded batches until no
eligible source row remains or its execution budget expires. The response
distinguishes exact processed-row counters from the boolean
telemetryBacklogPending and the drained, deadline, or batch-limit stop
reason. Concurrent cron calls cannot count the same source row twice: the
hourly merge uses a nonblocking advisory lock, a post-lock row recheck, and an
all-or-nothing delete. Terminal summaries read detailed and hourly rows in a
single database snapshot, so an atomic move is counted on exactly one side.
Their final write is optimistic: an accepted delayed window clears the old
summary and changes the session version, causing an older snapshot to become a
no-op until a later run recomputes it.
The production Vercel Pro schedule invokes it at minute 48 of every hour.
Events
{
"version": "1",
"environmentSlug": "demo-harbor",
"deploymentProviderId": "10001",
"deployment": {
"provider": "digitalocean",
"sizeSlug": "s-2vcpu-4gb",
"region": "sfo3",
"dropletHourlyUsd": 0.04167,
"volumeGiB": 10,
"monitorVersion": "1.14.0"
},
"events": [
{
"sourceEventId": "evt_demo_launched_001",
"occurredAt": "2026-07-23T17:50:00.000Z",
"kind": "launched",
"reason": "manual"
}
]
}
deployment is required when the payload includes launched and is rejected
when the payload has no launch event. It captures the bounded provider
metadata and launch-time rate needed to account for a short-lived deployment
before the next inventory poll. The optional Monitor version lets the console
distinguish not configured from configured but waiting. It never carries a
provider credential or release checksum.
Supported kinds:
launched;sunk;session-extended(manual, withextendedByMinutesand UTCexpiresAt);reaper-warning;reaper-destroyed;seed-restored;heartbeat-recovered;launch-failed(a launch that ran nothing: it needs nodeploymentProviderIdand nodeployment, never moves a deployment's lifecycle, and appears in the environment history so spend that produced no session is visible).
Supported reasons are manual, idle, session-expired, provider, health, seed, and
unknown.
Suite runs
{
"version": "1",
"environmentSlug": "demo-harbor",
"suiteName": "checkout-e2e",
"runs": [
{
"sourceRunId": "run_demo_20260723_001",
"specName": "create-order",
"status": "passed",
"durationMs": 18420,
"metricName": "workflow-latency",
"metricMs": 1220,
"variant": "baseline",
"artifacts": [
{
"$schema": "urn:filecheap.dev:artifact-ref:v1",
"version": 1,
"provider": "fcheap-local",
"uri": "fcheap://stash/report_20260723_184500.123456789_0123456789abcdef01234567",
"artifact_id": "report_20260723_184500.123456789_0123456789abcdef01234567",
"kind": "cairntrace.run",
"producer": {
"tool": "cairntrace",
"native_schema": "urn:cairntrace.dev:run:v1",
"native_id": "run_demo_20260723_001",
"entrypoint": "run.json"
}
}
]
}
]
}
Each run may also carry the optional failure, steps, labels and
runner fields described in Suites and artifacts;
the server redacts failure.message again and attributes the run to the box
it ran on.
A request accepts up to 250 runs; each run accepts up to 20 unique
ArtifactRefV1 envelopes. The server validates the exact schema, provider
identity rules, stable URLs, safe producer metadata, limits, status values,
and duplicates before persisting data.
ArtifactRefV1 is the canonical contract for every new producer. During the
compatibility release, this endpoint also accepts only the exact historical
{kind, store: "fcheap-local", stashId} and
{kind, store: "link", url} locators and normalizes them immediately to V1.
Unknown fields, unsafe URLs, invalid IDs, and identities that collide after
conversion remain invalid. This legacy transport path is deprecated and will
be removed after all deployed producers use ArtifactRefV1 and historical
pre-fingerprint rows have been remediated.
Fleet
GET /api/fleet returns:
{
"mode": "demo",
"generatedAt": "2026-07-23T18:21:00.000Z",
"totals": {
"environments": 1,
"afloat": 1,
"stale": 0,
"sunk": 0,
"unknown": 0,
"estimatedHourlyUsd": 0.04,
"estimatedMonthToDateUsd": 0.62
},
"environments": []
}
IP addresses, names, and stack details belong only to the authenticated response. Public metadata does not include them.
Login
URL-encoded form:
token=<admin-token>
next=/app
next is optional and accepts only relative paths that begin with /, never
//. Successful authentication creates the chalupa_admin HttpOnly cookie.
Do not send the token in a query string.
Errors
The API uses semantic HTTP status codes:
| Code | Case |
|---|---|
202 |
Ingest accepted. |
400 |
Malformed JSON. |
401 |
Invalid session or signature. |
409 |
HMAC nonce already claimed, deployment assigned to another environment, telemetry waiting for launch registration (telemetry_deployment_unavailable), an ended deployment outside its final grace period (telemetry_deployment_ended), a Monitor producer version that differs from the deployment pin (telemetry_monitor_version_mismatch), or a run/window identity retried with different normalized content (run_ingest_conflict or telemetry_ingest_conflict). |
413 |
Body exceeds the endpoint limit, or a deployment has exhausted its bounded log or telemetry storage quota. Log and CI artifact uploads also return 413 storage_budget_exceeded when the organization's retained bytes would pass its plan budget (1 GiB Free, 10 GiB Solo, 50 GiB Crew). Replays of bytes already accepted are not counted again. |
422 |
Invalid content type, UTF-8, or schema. |
503 |
Writes deliberately disabled in demo mode. |
Messages must never reveal keys, expected signatures, or sensitive content.
Inference summaries
POST /api/ingest/inference accepts a signed JSON body of at most 16 KiB. It uses the existing HMAC, environment-key binding and nonce protection. Its strict body contains version: "1", environmentSlug, deployment: { provider: "digitalocean", providerId }, and summary.
summary contains sourceRunId, model, optional digest, contextSize, status, stopReason, truncated, promptTokens, evalTokens, toolCalls, toolErrors, toolCallsOmitted, and optional timing. Timing fields are ttftMs, loadMs, promptEvalMs, evalMs and totalMs. Status is settled, canceled, failed or not_admitted; it never represents a test-quality verdict.
The deployment must already belong to the authenticated organization and environment. An accepted report returns HTTP 202 with accepted, deduplicated and an id. A different summary under the same deployment and source ID returns HTTP 409. Late receipts do not reactivate ended deployments. Receipt text, errors, tool arguments, tool output and paths are not accepted. Use chalupa inference report --dry-run to inspect the projection before sending it.
Heartbeats optionally include gpu: { state, devices }. State is observed or unavailable; unavailable requires an empty device list. Each of at most eight devices has id, name, and optional utilizationPercent, memoryUsedMiB, memoryTotalMiB, temperatureC and powerWatts. Missing sensor fields are unknown rather than zero. These samples follow the existing 48-hour heartbeat retention.
Account provider token
These endpoints require Authorization: Bearer <account session token> from
hosted account authentication. They use the session's default organization,
regardless of the organization cookie. No organization returns HTTP 400 with
error.code: "organization_required"; missing or inactive authentication returns
401. digitalocean and hetzner are supported, one credential per provider per
organization; ?provider= (GET/DELETE) or the body's provider (PUT) selects
which. Any other value returns 422. Responses use Cache-Control: no-store.
| Method | Path | Result (HTTP 200) |
|---|---|---|
GET |
/api/account/provider-token?provider=digitalocean|hetzner |
{ provider, status, createdAt?, rotatedAt?, reaper } |
PUT |
/api/account/provider-token |
DigitalOcean: { provider: "digitalocean", status: "active", reaper: "armed", scopesRequired: ["droplet:read", "droplet:delete"] }. Hetzner: { provider: "hetzner", status: "active", reaper: "armed", requirement: "Read & Write API token of a Hetzner project dedicated to Chalupa" } |
DELETE |
/api/account/provider-token?provider=digitalocean|hetzner |
{ provider, status: "revoked", reaper: "not-armed" } |
GET status is active, none, revoked, or invalid; reaper is armed only
for an active credential. Dates are ISO 8601 strings. No response returns token
material. PUT accepts { "provider": "digitalocean" | "hetzner", "token": "your_provider_token_here" }
and permits five attempts per account session per ten-minute window (429 after
that). Invalid shapes return 422. The body limit is 2 KiB.
PUT validates against a read-only, label/tag-filtered endpoint on the declared provider, with a ten-second timeout:
- DigitalOcean:
GET https://api.digitalocean.com/v2/droplets?tag_name=chalupa&per_page=1 - Hetzner:
GET https://api.hetzner.cloud/v1/servers?label_selector=chalupa%3Dtrue&per_page=1
A 401/403 from the declared provider returns HTTP 400:
{
"code": "token_rejected",
"detail": "DigitalOcean refused the token; it needs the custom scopes droplet:read and droplet:delete"
}
Hetzner's detail reads: "Hetzner refused the token; it needs a Read & Write
API token from a Hetzner project dedicated to Chalupa."
Network failures and other unsuccessful provider responses return 503 with
code: "provider_unavailable". This read probe cannot prove delete permission
for either provider; select both custom scopes in DigitalOcean, or a Read &
Write token in Hetzner. A rejected delete later disarms that provider's
credential and records a reaper-warning with reason provider — this is the
only way a read-only Hetzner token (which cannot be caught at PUT time) is
ever caught, since Hetzner tokens cannot be introspected for write permission
without performing a mutation.
This is opt-in, per provider. A DigitalOcean token can read and delete
droplets throughout its account; a Hetzner token can read and delete every
server in its project — Hetzner has no finer-grained scope, so use a project
dedicated to Chalupa. Chalupa uses either credential only to destroy
Chalupa-tagged (DigitalOcean) or Chalupa-labeled (Hetzner, chalupa=true or
chalupa-env) compute registered to that organization, under that same
provider, after session expiry, an idle deadline, or an authorized CI
onFinish policy. The token is encrypted at rest with AES-256-GCM under
CHALUPA_INGEST_MASTER_KEY and never shown again. PUT replaces the stored
credential for that provider and records reaper-armed; DELETE marks it
revoked and records a manual reaper-warning when a row exists for that
provider. A DigitalOcean and a Hetzner credential for the same organization
are independent: revoking or invalidating one never touches the other, and
the reaper only ever sweeps a provider's candidates with that provider's own
credential. Delete the token at the provider for instant provider-side
revocation. Console revocation cannot cancel an already-started provider request.
Managed GPU account API
Both endpoints use an account session in Authorization: Bearer <token> and the
account's first customer organization (the internal platform organization is never a
billing target). X-Chalupa-Organization: <slug or id> picks another organization the
account belongs to. Cookies do not select the organization. They
return 401 without authentication, 400 organization_required without a default
organization, and 403 for the internal platform organization.
GET /api/account/managed/balance
Available to every customer organization, including Community. Returns 200:
{
"enabled": true,
"currency": "usd",
"balanceCents": 0,
"updatedAt": null,
"role": "owner",
"launchBlocked": null,
"plan": { "key": "community", "eligible": false },
"topUpOptionsCents": [1000, 2500, 5000],
"rates": [
{ "tier": "gpu-small", "gpu": "RTX 4000 Ada", "vramGiB": 20, "providerListCentsPerHour": 76, "centsPerHour": 95 },
{ "tier": "gpu-large", "gpu": "RTX 6000 Ada", "vramGiB": 48, "providerListCentsPerHour": 157, "centsPerHour": 197 },
{ "tier": "gpu-max", "gpu": "H100", "vramGiB": 80, "providerListCentsPerHour": 441, "centsPerHour": 552 }
],
"storageCentsPerGibMonth": 13,
"ledger": []
}
role is the caller's role in the organization. launchBlocked is null when a
launch from this caller would be admitted, or { "code", "detail" } with the
same code and text the launch route would answer: managed_launch_owner_only
for a member, or managed_launch_disputed while a payment dispute is open. The
CLI reads it before it estimates a launch.
Ledger entries contain id, kind, deltaCents, balanceAfterCents, createdAt,
and nullable note. Dates are ISO timestamps; updatedAt is null before the
first credit. The response never includes Stripe identifiers. Demo reads return
zero balance and an empty ledger.
POST /api/account/managed/top-up
Body: { "amountCents": 2500 }. Only integer amounts 1000, 2500, and 5000 are
accepted; invalid bodies return 400. Without an active Solo or Crew entitlement,
the endpoint returns 403 { "code": "plan_required", "detail": "Managed GPU needs an active Solo or Crew plan" }.
Top-up attempts are limited to five per organization per ten-minute window;
excess attempts return 429. Demo writes are disabled.
Success returns 200 { "url": "https://checkout.stripe.com/...", "amountCents": 2500, "currency": "usd" }.
Follow url to pay. The balance changes only after a verified paid Stripe webhook,
not when Checkout is created or the browser returns. No compute is provisioned.
Managed launch jobs
Account routes require an account session bearer token and use its default organization. Requests are strict JSON objects; unknown fields fail validation.
| Endpoint | Request | Success |
|---|---|---|
POST /api/account/managed/launch |
Launch specification below plus environmentSlug |
202 { jobId, status: "queued", estimateCents, centsPerHour, expiresAfterMinutes, environmentSlug } |
GET /api/account/managed/jobs/{id} |
UUID path parameter | 200 job view below; 404 for missing/other-organization jobs |
POST /api/account/managed/down |
{ environmentSlug } |
202 { jobId } |
POST /api/account/managed/allow |
{ environmentSlug, allowIpv4Cidr } |
202 { jobId } |
GET /api/account/managed/hosts |
None | 200 { hosts, jobs } |
GET /api/account/managed/capacity |
None | 200 { capacity: { [sizeSlug]: regions[] } | null, checkedAt: ISO8601 | null } |
Launch specification:
| Field | Constraint/default |
|---|---|
environmentSlug |
3–63 lowercase letters, digits or hyphens; starts/ends alphanumeric |
tier |
gpu-small, gpu-large, gpu-max |
region |
ca-east (default), us-east, eu-west, us-south; older region spellings are still accepted |
model |
1–128 letters, digits, dots, underscores, colons, slashes or hyphens |
contextSize |
"auto" or integer 2048–262144; default "auto" |
cacheGb |
Optional integer 20–500; omitted means temporary host storage, with no retained volume |
maxModelBytes |
Optional positive integer, at most 500 GiB in bytes; defaults to the requested cache capacity or 60 GiB without a cache |
expiresAfterMinutes |
Integer 15–480 |
publicKey |
OpenSSH ssh-ed25519 key, single line, optional comment up to 64 characters |
allowIpv4Cidr |
One IPv4 address with /32 |
agent |
Optional opencode or omp |
Launch checks run in this order, and each refusal has a stable code:
- Only organization owners may launch: members get 403
managed_launch_owner_only. - The tier must be verified for managed launches: 422
managed_tier_unavailable. - Billing must be available: with reservations off, launches fail closed with 503
managed_billing_unavailable. - Plan eligibility: 403
plan_required. - Launch rate limit: ten attempts per organization per minute (429).
- Host limits: 409
host_activefor an environment with a live host, 409host_capfor the plan's concurrent host cap, 409cache_capfor the plan's retained model caches. - Current GPU capacity: 409
no_capacity. - Balance, checked with the hold: 402
{ code: "insufficient_balance", requiredCents, balanceCents, topUpOptionsCents }. - An open payment dispute freezes new launches: 409
managed_launch_disputed, until Stripe closes it. - A pending job on the same environment: 409
already_queued.
Down and allow reuse the last launch specification; no active host or running
launch returns 404 no_host.
Capacity pre-flight
After balance validation, a known unavailable tier/region returns HTTP 409:
{
"code": "no_capacity",
"requested": { "tier": "gpu-large", "region": "tor1", "gpu": "RTX 6000 Ada" },
"alternatives": [
{ "tier": "gpu-small", "region": "tor1", "gpu": "RTX 4000 Ada", "vramGiB": 20, "centsPerHour": 95, "fitsModel": false },
{ "tier": "gpu-large", "region": "nyc2", "gpu": "RTX 6000 Ada", "vramGiB": 48, "centsPerHour": 197, "fitsModel": true },
{ "tier": "gpu-max", "region": "tor1", "gpu": "H100", "vramGiB": 80, "centsPerHour": 552, "fitsModel": true }
],
"checkedAt": "2026-09-07T12:00:00.000Z"
}
Regions in this response use the slugs the launch endpoint accepts (tor1,
nyc2, ams3, atl1). The CLI shows them as ca-east, us-east, eu-west
and us-south. Clients that print a region should show the name.
No job, deployment or balance entry is created for this response. The existing
launch attempt rate limiter still applies. Alternatives include the same tier in
other regions and other tiers in the requested region and elsewhere, limited to
supported managed regions. They sort by centsPerHour, then prefer the requested
region, then sort regions alphabetically. fitsModel compares the estimate for
spec.model with the tier's VRAM; unknown estimates and hosted cloud tags are false.
GET /api/account/managed/capacity requires the same account session bearer token.
It returns GPU size slugs mapped to current region arrays, with checkedAt from
the probe. An empty region array means unavailable; capacity: null and
checkedAt: null mean unknown. The console uses DIGITALOCEAN_WRITE_TOKEN to query
GET https://api.digitalocean.com/v2/sizes?per_page=200, with a 10-second timeout
and a 60-second in-memory cache. HTTP, network, parse and incomplete-page failures
are unknown and never block launches. The account response is Cache-Control: no-store.
Capacity is advisory and may change before the worker provisions the host.
The job view contains id, kind, status, environmentSlug, spec, result, error, createdAt, finishedAt, deployment. Its spec omits publicKey but retains
allowIpv4Cidr. Deployment is { id, sessionExpiresAt } or null. Result is null,
a progress object with log, or the successful connection result plus log.
Log arrays retain the latest 200 lines, each at most 500 characters.
Hosts contain id, slug, ip, providerId, sessionExpiresAt, sizeSlug, region, launchedAt. The last ten job summaries contain id, kind, status, environmentSlug, createdAt, finishedAt, error.
Managed ingest key
POST /api/account/managed/ingest-key requires a bearer account session.
The organization comes from requireManagedAccount, including its existing
X-Chalupa-Organization membership selection; a body organization is rejected.
Send { "environmentSlug": "my-gpu" } (the managed slug rules apply).
Returns 200 { ingestKeyId, ingestKey, environmentSlug } with Cache-Control: no-store.
The plaintext key is returned only at issuance. No hash or encrypted envelope is returned.
Keys use the existing admin ingest repository's env_<slug>_<random> public ID
and 43-character base64url HMAC material, stored encrypted at rest. The label is
managed:<environmentSlug>; issuance atomically revokes previous keys with that
label in the same organization. Other labels and organizations are unaffected.
Requests are limited to five per minute per organization using IngestAttemptStore.
Authentication failures return 401, invalid bodies 422, throttling 429, and demo
writes 503. Deliver the material to /etc/chalupa/ingest.key and the public ID as
CHALUPA_INGEST_KEY_ID in the host's agent environment.