{"components":{"parameters":{"Id":{"in":"path","name":"id","required":true,"schema":{"type":"string"}},"Name":{"in":"path","name":"name","required":true,"schema":{"type":"string"}},"Project":{"description":"The project this request is about. Required for an admin key or a dashboard session (400 project_required without it); a write or read key may omit it or name only its own project (403 key_project_mismatch).","in":"header","name":"AGIdock-Project","required":false,"schema":{"type":"string"}}},"responses":{"CreditCutoff":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}},"description":"Credits are exhausted; top up to resume. Running VMs are not killed by the cap. Only an operation that lists this response can answer it: reads, the calls that end the cap, and all of /v1/support and /v1/compliance never do."},"NotFound":{"content":{"application/problem+json":{"schema":{"allOf":[{"$ref":"#/components/schemas/Problem"},{"properties":{"code":{"examples":["route_unknown","vm_not_found","volume_not_found"]}}}]}}},"description":"No such resource in this project: the one a write or read key was minted into, or the one the AGIdock-Project header names. A resource that exists but belongs to another project or account is reported as not found, never as forbidden, so ids cannot be probed for existence. A path no operation describes answers route_unknown. Codes include: route_unknown, vm_not_found, volume_not_found."}},"schemas":{"CodeRequest":{"properties":{"code":{"description":"current TOTP code; required once an authenticator is confirmed","type":"string"}},"type":"object"},"DNSRecord":{"properties":{"kind":{"description":"dkim or dmarc","type":"string"},"name":{"type":"string"},"note":{"type":"string"},"split":{"description":"pre-quoted 255-character strings, for DNS UIs that will not split a 2048-bit key","type":"string"},"type":{"type":"string"},"value":{"type":"string"}},"type":"object"},"NameRequest":{"properties":{"name":{"type":"string"}},"required":["name"],"type":"object"},"PaymentMethodStatus":{"properties":{"detail":{"type":"string"},"redirect_url":{"description":"Stripe's hosted card page; the card is saved on the webhook, not on the return","type":"string"},"refill_disabled":{"description":"removing the card switches auto-refill off in the same transaction","type":"boolean"},"status":{"type":"string"}},"type":"object"},"Price":{"properties":{"currency":{"type":"string"},"meter":{"type":"string"},"price_micro":{"description":"millionths of a credit per unit of this meter; the -hours meters price one unit for one hour, egress-gb and emails-sent price one event","type":"integer"}},"type":"object"},"Problem":{"description":"RFC 7807 problem document; all four fields always present. Branch on `code`, act on `detail` — `title` and `status` only restate the response line. Some errors add machine-readable fields beside these at the top level.","properties":{"code":{"type":"string"},"detail":{"type":"string"},"status":{"type":"integer"},"title":{"type":"string"}},"type":"object"},"Project":{"properties":{"archived":{"description":"an archived project keeps its data everywhere else, but mints no new keys and its existing keys are revoked","type":"boolean"},"created_at":{"format":"date-time","type":"string"},"id":{"type":"string"},"name":{"type":"string"}},"type":"object"},"SessionResponse":{"properties":{"expires_at":{"format":"date-time","type":"string"},"session_token":{"description":"bearer token for dashboard sessions; API keys are minted separately","type":"string"}},"type":"object"},"SnapVolume":{"properties":{"size_gb":{"type":"integer"},"volume_id":{"type":"string"}},"type":"object"},"SnapshotOpResult":{"properties":{"detail":{"type":"string"},"name":{"type":"string"},"status":{"type":"string"}},"type":"object"},"StatusDetail":{"properties":{"detail":{"type":"string"},"status":{"description":"stable token, safe to branch on","type":"string"}},"type":"object"},"StatusResponse":{"properties":{"status":{"type":"string"}},"type":"object"},"TOTPResponse":{"properties":{"detail":{"type":"string"},"otpauth_url":{"description":"present on the first call; add it to an authenticator","type":"string"},"status":{"description":"present once enrollment is confirmed","type":"string"},"stepped_up_until":{"description":"confirming is itself a proof of possession, so it elevates a session","format":"date-time","type":"string"}},"type":"object"},"TicketAck":{"properties":{"state":{"type":"string"},"status":{"type":"string"}},"type":"object"},"VMSummary":{"properties":{"cpu":{"type":"integer"},"created_at":{"format":"date-time","type":"string"},"disk_gb":{"type":"integer"},"health":{"description":"ok, or unreachable when the hypervisor did not answer","type":"string"},"id":{"type":"string"},"image":{"type":"string"},"ip":{"description":"null until an address is bound","type":"string"},"name":{"type":"string"},"power":{"description":"live from the hypervisor: running, stopped, or unknown when it could not be reached","type":"string"},"ram_mb":{"type":"integer"},"state_detail":{"type":"string"},"status":{"description":"creating, active or deleting; a deleted VM 404s instead. Stuck in creating or deleting means the workflow died partway — DELETE resumes it","type":"string"}},"type":"object"},"Volume":{"properties":{"created_at":{"format":"date-time","type":"string"},"id":{"type":"string"},"name":{"type":"string"},"size_gb":{"type":"integer"},"state":{"description":"creating, detached or attached; a deleted volume 404s instead","type":"string"},"state_detail":{"type":"string"},"vm_id":{"description":"null while detached","type":"string"}},"type":"object"},"VolumeActionResult":{"properties":{"detail":{"type":"string"},"size_gb":{"description":"the size the volume is at after this action; unchanged by attach and detach","type":"integer"},"state":{"type":"string"},"vm_id":{"type":"string"}},"type":"object"}},"securitySchemes":{"bearer":{"bearerFormat":"opaque","description":"An API key (agd_...) or a dashboard session token (sess_...).","scheme":"bearer","type":"http"}}},"info":{"description":"The agent-facing API for AGIdock: plain Linux VMs, block volumes,\npublic IPv4, and an outbound email relay with warmed reputation.\n\nAuth: `Authorization: Bearer \u003ctoken\u003e`. API keys (agd_...) are the normal agent\ncredential; session tokens (sess_...) come from the dashboard. Human-only operations\nreject API keys with 403 human_required — moving money, and the account's\nauthenticator. Step-up is a separate rule that asks only how recently a session\nverified a code, and never challenges an API key.\n\nAn account can hold more than one project (POST /v1/projects), and VMs, volumes,\nemail, usage, tickets and compliance cases each live in one. A read or write key\nis pinned to the project it was minted into for everything it does; naming\nanother is 403 key_project_mismatch. An admin key and a dashboard session belong\nto the account instead: they name the project each request is about with the\n`AGIdock-Project: \u003cproject id\u003e` header, and a project-scoped operation\nwithout it is 400 project_required. A project the account does not hold is\n404 project_not_found.\n\nErrors are RFC 7807 problem documents with a stable `code` and a `detail` that\nsays what to do next. Send an Idempotency-Key header on every mutating call; a\nrepeat with the same key returns the first result.\n\nFour responses are universal and so are not repeated on each operation: 400 the\nrequest is malformed or violates a documented constraint, 401 the bearer token\nis missing or invalid, 403 authenticated but not permitted (wrong role, or\nstep-up or a human required), 429 rate limited — retry after the Retry-After\nheader.\n\nEvery mutating call adds two more. 409: the request is well formed and the\ncurrent state, or a backing system, refuses it — branch on code, which is\nstable, and read detail, which names what is in the way. Some 409s mean\nnothing was applied and the identical request is the retry (another call holds\nthe resource, or an earlier request with this key is still running); the rest\nhave to be fixed first. Failures originating in a backing system answer 409\nrather than 5xx so their detail survives the edge intact. Codes include\ncreate_failed, hypervisor_error, idempotency_in_progress, idempotency_key_reuse,\nmust_detach_first, operation_in_progress, vm_has_volumes, vm_not_stopped and\nvolume_busy. 503 service_saturated: the service is already running as many\nconcurrent operations as it can and declined to start another — nothing was\napplied, and unlike the 409 timing codes this is total load, so back off before\nretrying.\n\nWhat an operation lists is what varies: 402 and 404.\n\nThis document is the route list, not the manual. The guides at\nhttps://agidock.cloud/docs/index.md carry sizing, procedure, and the error codes\nworth branching on.","title":"AGIdock API","version":"1.0.0"},"openapi":"3.1.0","paths":{"/v1/account":{"delete":{"responses":{"202":{"content":{"application/json":{"schema":{"properties":{"detail":{"type":"string"},"executes":{"description":"end of the grace period; cancellable until then","format":"date-time","type":"string"},"status":{"type":"string"}},"type":"object"}}},"description":"Success."},"402":{"$ref":"#/components/responses/CreditCutoff"}},"summary":"Schedule account deletion (GDPR self-service), cancellable during the grace period. Admin-role keys and dashboard sessions only."},"get":{"responses":{"200":{"content":{"application/json":{"schema":{"properties":{"created_at":{"format":"date-time","type":"string"},"delete_requested_at":{"description":"present only while a deletion is scheduled","format":"date-time","type":"string"},"email":{"type":"string"},"id":{"type":"string"},"status":{"type":"string"},"tos_version":{"type":"string"},"totp_enabled":{"type":"boolean"}},"type":"object"}}},"description":"Success."}},"summary":"Account state: id, email, status, TOTP enrollment."}},"/v1/account/delete/cancel":{"post":{"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusDetail"}}},"description":"Success."},"402":{"$ref":"#/components/responses/CreditCutoff"}},"summary":"Cancel a scheduled deletion. Admin-role keys and dashboard sessions only."}},"/v1/account/export":{"post":{"responses":{"200":{"content":{"application/json":{"schema":{"properties":{"account":{"additionalProperties":true,"description":"the account row as stored, minus secrets","type":"object"},"exported_at":{"format":"date-time","type":"string"},"projects":{"description":"one row per project the account holds, active or archived — the same rows GET /v1/projects lists","items":{"additionalProperties":true,"type":"object"},"type":"array"}},"type":"object"}}},"description":"Success."},"402":{"$ref":"#/components/responses/CreditCutoff"}},"summary":"Request a data export (GDPR)."}},"/v1/account/freeze":{"post":{"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusDetail"}}},"description":"Success."},"402":{"$ref":"#/components/responses/CreditCutoff"}},"summary":"Self-freeze: stop the account acting. The human kill switch. Admin-role keys and dashboard sessions only."}},"/v1/account/keys":{"get":{"responses":{"200":{"content":{"application/json":{"schema":{"properties":{"keys":{"items":{"properties":{"created_at":{"format":"date-time","type":"string"},"id":{"type":"string"},"last_used_at":{"description":"absent until the key is first used","format":"date-time","type":"string"},"name":{"type":"string"},"prefix":{"description":"the leading characters of the key, for matching it to a caller","type":"string"},"project_id":{"description":"the project a write or read key is permanently scoped to; absent for an admin key, which belongs to the account","type":"string"},"rate_limit":{"type":"integer"},"role":{"description":"admin, write or read","type":"string"}},"type":"object"},"type":"array"}},"type":"object"}}},"description":"Success."}},"summary":"List API keys with last-used timestamps and the project each is scoped to. Never returns secrets."},"post":{"parameters":[{"$ref":"#/components/parameters/Project"}],"requestBody":{"content":{"application/json":{"schema":{"properties":{"name":{"type":"string"},"rate_limit":{"type":"integer"},"role":{"description":"admin, write or read; issuance is strictly downward","type":"string"}},"required":["role"],"type":"object"}}},"required":true},"responses":{"201":{"content":{"application/json":{"schema":{"properties":{"detail":{"type":"string"},"id":{"type":"string"},"key":{"description":"the secret, shown exactly once and never retrievable again","type":"string"},"project_id":{"description":"the project a write or read key was minted into; absent for an admin key","type":"string"},"rate_limit":{"type":"integer"},"role":{"type":"string"}},"type":"object"}}},"description":"Success."},"402":{"$ref":"#/components/responses/CreditCutoff"}},"summary":"Mint an API key; the secret is returned exactly once. Admin-role keys and dashboard sessions only (403 admin_key_required) — a write or read key cannot mint at all. read = GET only, write = the whole operating surface, both inside the one project named by the AGIdock-Project header (400 project_required without it); admin = write across every project plus issuing and revoking keys, belonging to the account, so minted with no project named (400 admin_key_account_level). Issuance is strictly downward: a new admin key must come from a dashboard session (403 admin_key_human_only)."}},"/v1/account/keys/{id}":{"delete":{"parameters":[{"$ref":"#/components/parameters/Id"}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusDetail"}}},"description":"Success."},"402":{"$ref":"#/components/responses/CreditCutoff"},"404":{"$ref":"#/components/responses/NotFound"}},"summary":"Revoke an API key immediately. Admin-role keys and dashboard sessions only."}},"/v1/account/login":{"post":{"requestBody":{"content":{"application/json":{"schema":{"properties":{"email":{"type":"string"}},"required":["email"],"type":"object"}}},"required":true},"responses":{"202":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusDetail"}}},"description":"Success."},"402":{"$ref":"#/components/responses/CreditCutoff"}},"security":[],"summary":"Mail a sign-in or signup link. Never reveals whether the email has an account."}},"/v1/account/login/confirm":{"get":{"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SessionResponse"}}},"description":"Success."}},"security":[],"summary":"Exchange a magic-link token for a 7-day session token."}},"/v1/account/signup/complete":{"post":{"requestBody":{"content":{"application/json":{"schema":{"properties":{"signup_code":{"description":"optional; grants starter credits","type":"string"},"token":{"description":"from the emailed magic link","type":"string"},"tos_accepted":{"type":"boolean"}},"required":["token","tos_accepted"],"type":"object"}}},"required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SessionResponse"}}},"description":"Success."},"402":{"$ref":"#/components/responses/CreditCutoff"}},"security":[],"summary":"Accept ToS/AUP and create the account; optional signup code grants starter credits."}},"/v1/account/totp":{"delete":{"description":"Human-only: needs a dashboard session. API keys get 403 human_required.","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodeRequest"}}},"required":false},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TOTPResponse"}}},"description":"Success."},"402":{"$ref":"#/components/responses/CreditCutoff"}},"summary":"Remove the authenticator. A confirmed enrollment requires a current code in the body."},"post":{"description":"Human-only: needs a dashboard session. API keys get 403 human_required.","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TOTPResponse"}}},"description":"Success."},"402":{"$ref":"#/components/responses/CreditCutoff"}},"summary":"Begin TOTP enrollment; returns the otpauth URL and base32 secret once. Refuses while a confirmed authenticator exists (409 totp_already_enabled)."}},"/v1/account/totp/verify":{"post":{"description":"Human-only: needs a dashboard session. API keys get 403 human_required.","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodeRequest"}}},"required":true},"responses":{"200":{"content":{"application/json":{"schema":{"properties":{"detail":{"type":"string"},"status":{"type":"string"},"until":{"description":"step-up expires here; it takes effect immediately","format":"date-time","type":"string"}},"type":"object"}}},"description":"Success."}},"summary":"Verify a code: confirms enrollment and steps the session up for 15 minutes."}},"/v1/account/unfreeze":{"post":{"description":"Human-only: needs a dashboard session. API keys get 403 human_required.","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodeRequest"}}},"required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusDetail"}}},"description":"Success."}},"summary":"Lift a self-freeze. Reachable while suspended, by design. Takes a current TOTP code in the body as well: a suspended session cannot obtain step-up, and the code stands in for it."}},"/v1/billing/balance":{"get":{"responses":{"200":{"content":{"application/json":{"schema":{"properties":{"balance_micro":{"description":"millionths of a credit; 1 credit = 1 USD","type":"integer"},"burn_rate_micro_per_hour":{"type":"integer"},"discount":{"properties":{"percent":{"type":"integer"},"until":{"format":"date-time","type":"string"}},"type":"object"},"grant_micro":{"description":"the part of the balance that came from grants rather than money","type":"integer"},"paid_micro":{"type":"integer"},"payment_completed":{"description":"at least one real payment has settled; this is what unlocks outbound email","type":"boolean"},"payment_method_set":{"description":"a card is on file, which may be true before any payment settles","type":"boolean"},"runway_hours":{"description":"null when nothing is burning, so runway is unbounded","type":"integer"},"state":{"type":"string"}},"type":"object"}}},"description":"Success."}},"summary":"Credit balance, grant vs paid split, burn rate and runway estimate. discount describes a signup code's running top-up bonus while its window is open."}},"/v1/billing/ledger":{"get":{"responses":{"200":{"content":{"application/json":{"schema":{"properties":{"entries":{"items":{"properties":{"amount_micro":{"description":"signed; debits are negative","type":"integer"},"detail":{"additionalProperties":true,"type":"object"},"id":{"type":"integer"},"kind":{"description":"topup, auto_refill, signup_grant, topup_bonus, debit, adjustment or expiry","type":"string"},"meter":{"type":"string"},"ref":{"type":"string"},"ts":{"format":"date-time","type":"string"}},"type":"object"},"type":"array"},"next_cursor":{"description":"id of the last entry here; pass it back as ?cursor for the next page. It is zero only on an empty page, so stop when entries is shorter than limit","type":"integer"}},"type":"object"}}},"description":"Success."}},"summary":"Ledger entries, newest first over ?cursor\u0026limit — the agent-auditable money trail. Usage is debited hourly, so an unfiltered page is almost all sweep debits: ?kind=topup,adjustment keeps only the kinds named and ?exclude=debit drops them. Kinds: topup, auto_refill, signup_grant, topup_bonus, debit, adjustment, expiry."}},"/v1/billing/payment-method":{"delete":{"description":"Human-only: needs a dashboard session. API keys get 403 human_required.\n\nRequires step-up: the session must have verified TOTP within 15 minutes.","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PaymentMethodStatus"}}},"description":"Success."},"402":{"$ref":"#/components/responses/CreditCutoff"}},"summary":"Forget the saved card; disables auto-refill in the same transaction."},"get":{"description":"Human-only: needs a dashboard session. API keys get 403 human_required.","responses":{"200":{"content":{"application/json":{"schema":{"properties":{"card":{"description":"absent for a card saved before the details were read back","properties":{"brand":{"type":"string"},"country":{"type":"string"},"exp_month":{"type":"integer"},"exp_year":{"type":"integer"},"funding":{"type":"string"},"last4":{"type":"string"},"name":{"type":"string"},"postal_code":{"type":"string"}},"type":"object"},"payment_method_set":{"type":"boolean"}},"type":"object"}}},"description":"Success."}},"summary":"Which card is on file: brand, last four, expiry, funding, issuing country and the billing name and postal code the card was saved with. Human-only."},"post":{"description":"Human-only: needs a dashboard session. API keys get 403 human_required.\n\nRequires step-up: the session must have verified TOTP within 15 minutes.","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PaymentMethodStatus"}}},"description":"Success."}},"summary":"Begin saving a card: answers 202 with redirect_url, Stripe's hosted card page. The card is never entered here, so the method is saved on the webhook rather than on this call."}},"/v1/billing/prices":{"get":{"responses":{"200":{"content":{"application/json":{"schema":{"properties":{"effective_from":{"description":"when the prices below took effect","format":"date-time","type":"string"},"hours_per_month":{"description":"multiply an hourly rate by this for the monthly figure the pricing page quotes","type":"integer"},"note":{"type":"string"},"prices":{"description":"every meter, at the price charged now — one entry each","items":{"$ref":"#/components/schemas/Price"},"type":"array"},"scheduled":{"description":"announced changes, published at least 30 days before they take effect","items":{"properties":{"effective_from":{"format":"date-time","type":"string"},"prices":{"items":{"$ref":"#/components/schemas/Price"},"type":"array"}},"type":"object"},"type":"array"}},"type":"object"}}},"description":"Success."}},"security":[],"summary":"The public price book: every meter at the price charging now, dated once by effective_from, any announced change under scheduled, and hours_per_month for converting hourly rates to the monthly figures the pricing page quotes."}},"/v1/billing/refill":{"get":{"responses":{"200":{"content":{"application/json":{"schema":{"properties":{"amount_micro":{"type":"integer"},"enabled":{"type":"boolean"},"monthly_cap_micro":{"type":"integer"},"refilled_this_month_micro":{"type":"integer"},"threshold_micro":{"type":"integer"}},"type":"object"}}},"description":"Success."}},"summary":"Current auto-refill settings."},"put":{"description":"Human-only: needs a dashboard session. API keys get 403 human_required.\n\nRequires step-up: the session must have verified TOTP within 15 minutes.","requestBody":{"content":{"application/json":{"schema":{"properties":{"amount_micro":{"type":"integer"},"enabled":{"type":"boolean"},"monthly_cap_micro":{"description":"hard ceiling on automatic spend per month","type":"integer"},"threshold_micro":{"description":"refill when the balance falls below this","type":"integer"}},"required":["enabled","threshold_micro","amount_micro","monthly_cap_micro"],"type":"object"}}},"required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusDetail"}}},"description":"Success."}},"summary":"Configure auto-refill: threshold, amount, and a hard monthly cap."}},"/v1/billing/topup":{"post":{"description":"Human-only: needs a dashboard session. API keys get 403 human_required.\n\nRequires step-up: the session must have verified TOTP within 15 minutes.","requestBody":{"content":{"application/json":{"schema":{"properties":{"amount_micro":{"description":"millionths of a credit; 1 credit = 1 USD","type":"integer"}},"required":["amount_micro"],"type":"object"}}},"required":true},"responses":{"200":{"content":{"application/json":{"schema":{"properties":{"amount_micro":{"description":"0 while status is pending; the charge settles on the payment webhook","type":"integer"},"bonus_micro":{"description":"a signup code's running discount, added on top","type":"integer"},"detail":{"type":"string"},"redirect_url":{"description":"Stripe's hosted payment page; credits post on the webhook, not on the return","type":"string"},"status":{"description":"completed or pending","type":"string"}},"type":"object"}}},"description":"Success."}},"summary":"Charge the saved card and add credits. Requires a card on file (409 payment_method_required). A signup code's discount adds bonus_micro credits on top. Answers 202 with redirect_url — Stripe's hosted payment page — when the payer has to authorise the charge; the credits post on the webhook, not on the redirect."}},"/v1/compliance/cases":{"get":{"parameters":[{"$ref":"#/components/parameters/Project"}],"responses":{"200":{"content":{"application/json":{"schema":{"properties":{"cases":{"items":{"properties":{"body":{"type":"string"},"closed_at":{"format":"date-time","type":"string"},"created_at":{"format":"date-time","type":"string"},"deadline":{"description":"miss it and the case moves to deadline-exceeded","format":"date-time","type":"string"},"id":{"type":"string"},"messages":{"items":{"properties":{"author":{"description":"tenant|operator","type":"string"},"body":{"type":"string"},"created_at":{"format":"date-time","type":"string"}},"type":"object"},"type":"array"},"state":{"description":"open|deadline-exceeded|closed","type":"string"},"type":{"description":"spam|ddos|dmca|fraud","type":"string"},"waiting_on":{"description":"customer|admin — whose turn it is","type":"string"}},"type":"object"},"type":"array"},"note":{"type":"string"}},"type":"object"}}},"description":"Success."}},"summary":"Abuse and DMCA cases opened against the account, with their response deadlines. Opening one puts it here and mails the account owner, so nothing is enforced that the tenant cannot read."}},"/v1/compliance/cases/{id}/response":{"post":{"parameters":[{"$ref":"#/components/parameters/Id"},{"$ref":"#/components/parameters/Project"}],"requestBody":{"content":{"application/json":{"schema":{"properties":{"response":{"type":"string"}},"required":["response"],"type":"object"}}},"required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusDetail"}}},"description":"Success."},"404":{"$ref":"#/components/responses/NotFound"}},"summary":"Answer a case. Replies append to the thread; they never replace what was said before. Reachable while capped or suspended, by design — the deadline runs in both states."}},"/v1/email/credentials":{"get":{"parameters":[{"$ref":"#/components/parameters/Project"}],"responses":{"200":{"content":{"application/json":{"schema":{"properties":{"credentials":{"items":{"properties":{"created_at":{"format":"date-time","type":"string"},"domain":{"description":"the only domain this credential may put in From","type":"string"},"id":{"type":"string"},"username":{"type":"string"}},"type":"object"},"type":"array"},"host":{"description":"submission host these credentials authenticate to","type":"string"},"port":{"description":"SMTP submission port; STARTTLS","type":"integer"}},"type":"object"}}},"description":"Success."}},"summary":"List SMTP credentials (never the secrets)."},"post":{"parameters":[{"$ref":"#/components/parameters/Project"}],"requestBody":{"content":{"application/json":{"schema":{"properties":{"domain":{"description":"a sending domain already registered on this account; the credential may put only this domain in From","type":"string"}},"required":["domain"],"type":"object"}}},"required":true},"responses":{"201":{"content":{"application/json":{"schema":{"properties":{"detail":{"type":"string"},"domain":{"type":"string"},"host":{"description":"submission host to connect to","type":"string"},"id":{"type":"string"},"password":{"description":"shown exactly once; it is not stored and cannot be read back","type":"string"},"port":{"description":"SMTP submission port; STARTTLS","type":"integer"},"username":{"type":"string"}},"type":"object"}}},"description":"Success."},"402":{"$ref":"#/components/responses/CreditCutoff"}},"summary":"Create SMTP submission credentials bound to one sending domain. Sending also requires at least one settled payment and that the domain has verified."}},"/v1/email/credentials/{id}":{"delete":{"parameters":[{"$ref":"#/components/parameters/Id"},{"$ref":"#/components/parameters/Project"}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusResponse"}}},"description":"Success."},"402":{"$ref":"#/components/responses/CreditCutoff"},"404":{"$ref":"#/components/responses/NotFound"}},"summary":"Revoke SMTP credentials."}},"/v1/email/domains":{"get":{"parameters":[{"$ref":"#/components/parameters/Project"}],"responses":{"200":{"content":{"application/json":{"schema":{"properties":{"detail":{"type":"string"},"domains":{"items":{"properties":{"domain":{"type":"string"},"fail_reason":{"type":"string"},"id":{"type":"string"},"last_checked_at":{"format":"date-time","type":"string"},"records":{"items":{"$ref":"#/components/schemas/DNSRecord"},"type":"array"},"selector":{"type":"string"},"status":{"description":"pending or verified; only verified domains may send","type":"string"},"verified_at":{"format":"date-time","type":"string"}},"type":"object"},"type":"array"}},"type":"object"}}},"description":"Success."}},"summary":"Sending domains with verification status and the DKIM and DMARC records each needs. A verified domain whose records stop resolving returns to pending, which stops sending on it."},"post":{"parameters":[{"$ref":"#/components/parameters/Project"}],"requestBody":{"content":{"application/json":{"schema":{"properties":{"domain":{"description":"the domain to send From, e.g. example.com; a subdomain is registered separately","type":"string"}},"required":["domain"],"type":"object"}}},"required":true},"responses":{"201":{"content":{"application/json":{"schema":{"properties":{"detail":{"type":"string"},"domain":{"type":"string"},"id":{"type":"string"},"records":{"items":{"$ref":"#/components/schemas/DNSRecord"},"type":"array"},"selector":{"type":"string"},"status":{"type":"string"}},"type":"object"}}},"description":"Success."},"402":{"$ref":"#/components/responses/CreditCutoff"}},"summary":"Register a sending domain and get the two records to publish: the DKIM key generated here, and a DMARC policy — any policy, p=none included, since only the record's existence is checked. One at the organizational domain covers its subdomains. The domain verifies only once both resolve, and mail From an unverified domain is rejected at submission."}},"/v1/email/domains/{id}":{"delete":{"parameters":[{"$ref":"#/components/parameters/Id"},{"$ref":"#/components/parameters/Project"}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusResponse"}}},"description":"Success."},"402":{"$ref":"#/components/responses/CreditCutoff"},"404":{"$ref":"#/components/responses/NotFound"}},"summary":"Remove a sending domain and its signing key."}},"/v1/email/domains/{id}/verify":{"post":{"parameters":[{"$ref":"#/components/parameters/Id"},{"$ref":"#/components/parameters/Project"}],"responses":{"200":{"content":{"application/json":{"schema":{"properties":{"domain":{"type":"string"},"fail_reason":{"type":"string"},"id":{"type":"string"},"status":{"type":"string"}},"type":"object"}}},"description":"Success."},"402":{"$ref":"#/components/responses/CreditCutoff"},"404":{"$ref":"#/components/responses/NotFound"}},"summary":"Re-check the domain's DNS now instead of waiting for the periodic check. Both the DKIM key and a DMARC policy must resolve; fail_reason names whichever does not."}},"/v1/email/stats":{"get":{"parameters":[{"$ref":"#/components/parameters/Project"}],"responses":{"200":{"content":{"application/json":{"schema":{"properties":{"days":{"additionalProperties":{"properties":{"reject":{"type":"integer"},"reject_recipients":{"type":"integer"},"sent":{"type":"integer"},"sent_recipients":{"type":"integer"}},"type":"object"},"type":"object"},"note":{"type":"string"},"tier":{"properties":{"daily_limit":{"description":"recipients per day, not messages","type":"integer"},"frozen_reason":{"description":"present only while sending is frozen","type":"string"},"level":{"type":"integer"}},"type":"object"}},"type":"object"}}},"description":"Success."}},"summary":"Sent and rejected counts per day, as messages and as recipients — recipients being what the quota and the meter charge."}},"/v1/email/tier":{"get":{"parameters":[{"$ref":"#/components/parameters/Project"}],"responses":{"200":{"content":{"application/json":{"schema":{"properties":{"daily_limit":{"description":"recipients per day, not messages","type":"integer"},"frozen_reason":{"description":"present only while sending is frozen","type":"string"},"level":{"type":"integer"},"note":{"type":"string"},"recipients_today":{"description":"recipients sent since 00:00 UTC, against daily_limit","type":"integer"}},"type":"object"}}},"description":"Success."}},"summary":"Current sending tier and the limits that come with it."}},"/v1/images":{"get":{"responses":{"200":{"content":{"application/json":{"schema":{"properties":{"images":{"items":{"properties":{"id":{"type":"string"},"name":{"description":"alias that follows the newest version under that name","type":"string"},"released_at":{"format":"date-time","type":"string"},"version":{"type":"string"}},"type":"object"},"type":"array"},"note":{"type":"string"}},"type":"object"}}},"description":"Success."}},"summary":"Stock image catalog: the operating systems we build and keep patched. Pin an image by id, or use its name alias (for example \"ubuntu-lts\") to follow the newest version under that name. Everything listed is usable; a withdrawn image stops being listed. There is no custom image upload."}},"/v1/projects":{"get":{"responses":{"200":{"content":{"application/json":{"schema":{"properties":{"projects":{"items":{"$ref":"#/components/schemas/Project"},"type":"array"}},"type":"object"}}},"description":"Success."}},"summary":"List the account's projects, active and archived."},"post":{"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NameRequest"}}},"required":true},"responses":{"201":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Project"}}},"description":"Success."},"402":{"$ref":"#/components/responses/CreditCutoff"}},"summary":"Create a project. Admin-role keys and dashboard sessions only. Mint keys into it by naming it in the AGIdock-Project header on POST /v1/account/keys."}},"/v1/projects/{id}":{"put":{"parameters":[{"$ref":"#/components/parameters/Id"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NameRequest"}}},"required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Project"}}},"description":"Success."},"402":{"$ref":"#/components/responses/CreditCutoff"},"404":{"$ref":"#/components/responses/NotFound"}},"summary":"Rename a project. Admin-role keys and dashboard sessions only."}},"/v1/projects/{id}/archive":{"post":{"parameters":[{"$ref":"#/components/parameters/Id"}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusDetail"}}},"description":"Success."},"402":{"$ref":"#/components/responses/CreditCutoff"},"404":{"$ref":"#/components/responses/NotFound"}},"summary":"Archive a project: every key under it is revoked immediately and it stops being mintable into. Nothing elsewhere (VMs, volumes, email domains, tickets) is touched. Admin-role keys and dashboard sessions only."}},"/v1/projects/{id}/unarchive":{"post":{"parameters":[{"$ref":"#/components/parameters/Id"}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusDetail"}}},"description":"Success."},"402":{"$ref":"#/components/responses/CreditCutoff"},"404":{"$ref":"#/components/responses/NotFound"}},"summary":"Make an archived project mintable again. Does not restore the keys archiving revoked. Admin-role keys and dashboard sessions only."}},"/v1/support/tickets":{"get":{"parameters":[{"$ref":"#/components/parameters/Project"}],"responses":{"200":{"content":{"application/json":{"schema":{"properties":{"tickets":{"items":{"properties":{"created_at":{"format":"date-time","type":"string"},"id":{"type":"string"},"ref":{"description":"the resource the ticket is about, if any","type":"string"},"state":{"description":"open, answered or closed","type":"string"},"subject":{"type":"string"},"type":{"type":"string"},"updated_at":{"format":"date-time","type":"string"}},"type":"object"},"type":"array"}},"type":"object"}}},"description":"Success."}},"summary":"List the account's tickets and their states."},"post":{"parameters":[{"$ref":"#/components/parameters/Project"}],"requestBody":{"content":{"application/json":{"schema":{"properties":{"body":{"type":"string"},"ref":{"description":"the resource the ticket is about, if any","type":"string"},"subject":{"type":"string"},"type":{"type":"string"}},"required":["type","subject","body"],"type":"object"}}},"required":true},"responses":{"201":{"content":{"application/json":{"schema":{"properties":{"detail":{"type":"string"},"id":{"type":"string"},"state":{"type":"string"}},"type":"object"}}},"description":"Success."}},"summary":"Open a support ticket. Reachable while capped or suspended, by design."}},"/v1/support/tickets/{id}":{"get":{"parameters":[{"$ref":"#/components/parameters/Id"},{"$ref":"#/components/parameters/Project"}],"responses":{"200":{"content":{"application/json":{"schema":{"properties":{"account_id":{"type":"string"},"created_at":{"format":"date-time","type":"string"},"id":{"type":"string"},"messages":{"items":{"properties":{"author":{"description":"tenant or support","type":"string"},"body":{"type":"string"},"created_at":{"format":"date-time","type":"string"}},"type":"object"},"type":"array"},"ref":{"description":"the resource the ticket is about, if any","type":"string"},"state":{"description":"open, answered or closed","type":"string"},"subject":{"type":"string"},"type":{"type":"string"},"updated_at":{"format":"date-time","type":"string"}},"type":"object"}}},"description":"Success."},"404":{"$ref":"#/components/responses/NotFound"}},"summary":"Ticket state and the full message thread — the poll target."}},"/v1/support/tickets/{id}/close":{"post":{"parameters":[{"$ref":"#/components/parameters/Id"},{"$ref":"#/components/parameters/Project"}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TicketAck"}}},"description":"Success."},"404":{"$ref":"#/components/responses/NotFound"}},"summary":"Close a ticket."}},"/v1/support/tickets/{id}/messages":{"post":{"parameters":[{"$ref":"#/components/parameters/Id"},{"$ref":"#/components/parameters/Project"}],"requestBody":{"content":{"application/json":{"schema":{"properties":{"body":{"type":"string"}},"required":["body"],"type":"object"}}},"required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TicketAck"}}},"description":"Success."},"404":{"$ref":"#/components/responses/NotFound"}},"summary":"Reply to a ticket (reopens it if answered)."}},"/v1/usage":{"get":{"parameters":[{"$ref":"#/components/parameters/Project"}],"responses":{"200":{"content":{"application/json":{"schema":{"properties":{"from":{"format":"date-time","type":"string"},"items":{"items":{"properties":{"group":{"type":"string"},"meter":{"type":"string"},"provisional":{"description":"the still-open current hour; may change until it closes","type":"boolean"},"quantity":{"type":"number"}},"type":"object"},"type":"array"},"note":{"type":"string"},"to":{"format":"date-time","type":"string"}},"type":"object"}}},"description":"Success."}},"summary":"Metered usage over ?from\u0026to\u0026group_by=meter|resource|day. Usage is stored in hourly buckets and the window covers every hour it overlaps, so the response echoes the window actually used. Current-hour numbers are included and flagged provisional."}},"/v1/vms":{"get":{"parameters":[{"$ref":"#/components/parameters/Project"}],"responses":{"200":{"content":{"application/json":{"schema":{"properties":{"vms":{"items":{"$ref":"#/components/schemas/VMSummary"},"type":"array"}},"type":"object"}}},"description":"Success."}},"summary":"List the account's VMs with status, live power state, IP and health."},"post":{"parameters":[{"$ref":"#/components/parameters/Project"}],"requestBody":{"content":{"application/json":{"schema":{"properties":{"cpu":{"description":"omit it: derived from ram_mb, and sending a conflicting value is an error rather than an override","type":"integer"},"disk_gb":{"type":"integer"},"image":{"description":"an image id, or a name alias such as ubuntu-lts","type":"string"},"name":{"type":"string"},"ram_mb":{"type":"integer"},"ssh_keys":{"items":{"type":"string"},"type":"array"}},"required":["name","ram_mb","disk_gb","image","ssh_keys"],"type":"object"}}},"required":true},"responses":{"201":{"content":{"application/json":{"schema":{"properties":{"cpu":{"type":"integer"},"detail":{"type":"string"},"health":{"type":"string"},"id":{"type":"string"},"ip":{"type":"string"},"power":{"type":"string"},"status":{"type":"string"}},"type":"object"}}},"description":"Success."},"402":{"$ref":"#/components/responses/CreditCutoff"}},"summary":"Create a VM. Blocks until it is built and answers 201 with power=stopped; create, attach and start are separate calls. Size by RAM alone: ram_mb is a whole number of GB (min 1024, 1024 MB steps) and the vCPU count is derived as ceil(ram_gb/2). disk_gb starts at 24 in 8 GB steps and is fixed for the VM's life. The root disk is local NVMe, the fast tier: the OS, databases and small random I/O belong on it, and bulk data on a volume."}},"/v1/vms/{id}":{"delete":{"parameters":[{"$ref":"#/components/parameters/Id"},{"$ref":"#/components/parameters/Project"}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusDetail"}}},"description":"Success."},"402":{"$ref":"#/components/responses/CreditCutoff"},"404":{"$ref":"#/components/responses/NotFound"}},"summary":"Destroy a VM and its root disk; the IP returns to the pool. Nothing it holds is deleted on your behalf: 409 vm_has_snapshots while it has snapshots, 409 vm_has_volumes while a volume is attached, each listing what is left in the problem's extras and the order to clear it in."},"get":{"parameters":[{"$ref":"#/components/parameters/Id"},{"$ref":"#/components/parameters/Project"}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/VMSummary"}}},"description":"Success."},"404":{"$ref":"#/components/responses/NotFound"}},"summary":"Get one VM."}},"/v1/vms/{id}/actions":{"post":{"parameters":[{"$ref":"#/components/parameters/Id"},{"$ref":"#/components/parameters/Project"}],"requestBody":{"content":{"application/json":{"schema":{"properties":{"cpu":{"type":"integer"},"force":{"type":"boolean"},"ram_mb":{"type":"integer"},"type":{"description":"start, stop, reboot or resize","type":"string"}},"required":["type"],"type":"object"}}},"required":true},"responses":{"200":{"content":{"application/json":{"schema":{"properties":{"detail":{"type":"string"},"power":{"description":"the power state the action left the VM in","type":"string"},"status":{"type":"string"}},"type":"object"}}},"description":"Success."},"402":{"$ref":"#/components/responses/CreditCutoff"},"404":{"$ref":"#/components/responses/NotFound"}},"summary":"start | stop | reboot | resize. Resize sets ram_mb and the vCPU count follows; it sizes down as well as up, but the VM must be stopped first (409 vm_not_stopped). The root disk is never resizable. stop additionally requires TOTP step-up for dashboard sessions; the other actions do not."}},"/v1/vms/{id}/events":{"get":{"parameters":[{"$ref":"#/components/parameters/Id"},{"$ref":"#/components/parameters/Project"}],"responses":{"200":{"content":{"application/json":{"schema":{"properties":{"events":{"items":{"properties":{"detail":{"type":"string"},"ts":{"format":"date-time","type":"string"},"type":{"type":"string"}},"type":"object"},"type":"array"}},"type":"object"}}},"description":"Success."},"404":{"$ref":"#/components/responses/NotFound"}},"summary":"Provisioning and lifecycle events — where to look when creation fails."}},"/v1/vms/{id}/snapshot":{"get":{"parameters":[{"$ref":"#/components/parameters/Id"},{"$ref":"#/components/parameters/Project"}],"responses":{"200":{"content":{"application/json":{"schema":{"properties":{"snapshots":{"items":{"properties":{"created_at":{"format":"date-time","type":"string"},"detail":{"type":"string"},"name":{"type":"string"},"volumes":{"description":"null when the record is missing or unparsable, which refuses rollback","items":{"$ref":"#/components/schemas/SnapVolume"},"type":"array"}},"type":"object"},"type":"array"}},"type":"object"}}},"description":"Success."},"404":{"$ref":"#/components/responses/NotFound"}},"summary":"List snapshots, each with the volumes that were attached when it was taken — the layout a rollback would restore. A snapshot with no volume record cannot be rolled back and says so."},"post":{"parameters":[{"$ref":"#/components/parameters/Id"},{"$ref":"#/components/parameters/Project"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NameRequest"}}},"required":true},"responses":{"201":{"content":{"application/json":{"schema":{"properties":{"detail":{"type":"string"},"name":{"type":"string"},"volumes":{"items":{"$ref":"#/components/schemas/SnapVolume"},"type":"array"}},"type":"object"}}},"description":"Success."},"402":{"$ref":"#/components/responses/CreditCutoff"},"404":{"$ref":"#/components/responses/NotFound"}},"summary":"Snapshot the VM — the root disk and every attached volume. Crash-consistent: memory is not captured, so a rollback boots rather than resumes. Records the volumes attached and their sizes, so a rollback can check the layout still matches."}},"/v1/vms/{id}/snapshot/{name}":{"delete":{"parameters":[{"$ref":"#/components/parameters/Id"},{"$ref":"#/components/parameters/Name"},{"$ref":"#/components/parameters/Project"}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SnapshotOpResult"}}},"description":"Success."},"402":{"$ref":"#/components/responses/CreditCutoff"},"404":{"$ref":"#/components/responses/NotFound"}},"summary":"Delete a snapshot. Also the way to reach an older restore point: rollback accepts only the newest, so deleting the ones after it — newest first — is what makes an earlier one restorable. Irreversible."}},"/v1/vms/{id}/snapshot/{name}/rollback":{"post":{"parameters":[{"$ref":"#/components/parameters/Id"},{"$ref":"#/components/parameters/Name"},{"$ref":"#/components/parameters/Project"}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SnapshotOpResult"}}},"description":"Success."},"402":{"$ref":"#/components/responses/CreditCutoff"},"404":{"$ref":"#/components/responses/NotFound"}},"summary":"Roll back to a snapshot in place. Only the newest snapshot is restorable, so reaching an older one means deleting the ones after it first. The VM must be stopped (409 vm_not_stopped) and stays stopped. The snapshot restores the machine's size as well as its disks, so ram_mb and the derived vCPU count return to what they were and billing follows. Refused with 409 snapshot_volumes_changed if a volume has been attached since."}},"/v1/volumes":{"get":{"parameters":[{"$ref":"#/components/parameters/Project"}],"responses":{"200":{"content":{"application/json":{"schema":{"properties":{"volumes":{"items":{"$ref":"#/components/schemas/Volume"},"type":"array"}},"type":"object"}}},"description":"Success."}},"summary":"List volumes with attachment state."},"post":{"parameters":[{"$ref":"#/components/parameters/Project"}],"requestBody":{"content":{"application/json":{"schema":{"properties":{"name":{"type":"string"},"size_gb":{"type":"integer"}},"required":["name","size_gb"],"type":"object"}}},"required":true},"responses":{"201":{"content":{"application/json":{"schema":{"properties":{"detail":{"type":"string"},"id":{"type":"string"},"name":{"type":"string"},"size_gb":{"type":"integer"},"state":{"type":"string"},"vm_id":{"type":"string"}},"type":"object"}}},"description":"Success."},"402":{"$ref":"#/components/responses/CreditCutoff"}},"summary":"Create a block volume, detached. Bulk storage reached over the network: cheaper per GB and slower per operation than a VM's root disk, so it holds what grows rather than a database. Its life is independent of any VM: attach it, move it between VMs, outlive them. size_gb starts at 500 in 500 GB steps."}},"/v1/volumes/{id}":{"delete":{"parameters":[{"$ref":"#/components/parameters/Id"},{"$ref":"#/components/parameters/Project"}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/VolumeActionResult"}}},"description":"Success."},"402":{"$ref":"#/components/responses/CreditCutoff"},"404":{"$ref":"#/components/responses/NotFound"}},"summary":"Delete a detached volume and its data, irreversibly (409 must_detach_first while it is attached)."},"get":{"parameters":[{"$ref":"#/components/parameters/Id"},{"$ref":"#/components/parameters/Project"}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Volume"}}},"description":"Success."},"404":{"$ref":"#/components/responses/NotFound"}},"summary":"Get one volume."}},"/v1/volumes/{id}/actions":{"post":{"parameters":[{"$ref":"#/components/parameters/Id"},{"$ref":"#/components/parameters/Project"}],"requestBody":{"content":{"application/json":{"schema":{"properties":{"size_gb":{"description":"resize only, and only upward","type":"integer"},"type":{"description":"attach, detach or resize","type":"string"},"vm_id":{"description":"required for attach","type":"string"}},"required":["type"],"type":"object"}}},"required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/VolumeActionResult"}}},"description":"Success."},"402":{"$ref":"#/components/responses/CreditCutoff"},"404":{"$ref":"#/components/responses/NotFound"}},"summary":"attach | detach | resize. Attach works on a running VM; the device appears live, ready to partition and mount. Detach needs the VM stopped (409 vm_not_stopped): unmount in-guest first. Detach is also refused while a snapshot of the VM includes the volume (409 volume_in_snapshots): delete those snapshots first. Resize is grow-only, lands on a 500 GB step, and works live — then grow the filesystem in-guest."}}},"security":[{"bearer":[]}],"servers":[{"url":"https://api.agidock.cloud"}]}
