Vexelia API reference

API version 0.1.0

Generated from api/openapi.yaml. The panel uses this same API. Send requests to your Vexelia base URL; the examples use $VEXELIA_URL and a token in $VEXELIA_TOKEN.

Authentication

Create an API token at Account, API. Send it as Authorization: Bearer vx_live_.... A token works only on the operations its scopes open, inside the organisation it was made for. It never works on staff routes and never manages tokens. Operations marked step-up need a recent password check; only a token with a destructive scope satisfies that, because the check happened when the token was made.

Token scopes

ScopeAllowsNeedsDestructive
org:readSee the organisationorg.readno
servers:readList and see servers, orders and operationsservers.readno
servers:powerStart, stop and restart servers, and leave rescue modeservers.manageno
servers:rebuildRebuild servers onto another imageservers.manageyes: needs an IP allowlist or an expiry under 30 days
servers:destroyDestroy servers and cancel their renewalservers.destroyyes: needs an IP allowlist or an expiry under 30 days
firewall:readSee firewalls, their rules and which server uses whichservers.readno
firewall:writeCreate and change firewalls and rules, and attach them to serversservers.manageno
sshkeys:readSee SSH keyssshkeys.readno
sshkeys:manageAdd and remove SSH keyssshkeys.manageno

A refused call names the scope it lacked in fields.missing_scope.

Errors

Errors are RFC 9457 problem documents with a stable code; field errors are in fields.

CodeStatusMeaning
account_blocked402New orders are on hold for this account because of a card dispute or an unpaid balance. Check your billing page or contact support.
account_locked403This account is locked. Contact support to have it unlocked.
account_not_locked409That account is not locked.
addon_charge_open409A payment for the Backups addon is waiting. Pay it or let it expire first.
addon_not_enabled409The Backups addon is not on for this server.
address_in_use409That address is held by a customer's floating IP. It can't be changed here.
address_not_reserved409That address isn't reserved.
address_unavailable409That address is assigned, in quarantine or already reserved.
amount_below_minimum409This amount is below what the card processor can charge. Add credit to your account first.
app_already_installed409That app is already installed, or being installed, on this server.
app_change_busy409Another app is being installed, updated or uninstalled on this server. Wait until it finishes.
app_change_not_abandonable409That change has already finished or ended.
app_change_not_ready409Only an app that finished installing can be uninstalled or updated.
app_change_running409The server is still reporting this change as running. Wait a few minutes after its last progress, then try again.
app_change_unsupported409That app can't be uninstalled or updated from here (yet).
app_data_removal_changed409What an uninstall would delete changed since you looked. Check again and read the new list before you confirm.
app_data_removal_confirm_required409Removing the app's data can't be undone. Confirm that you want this, or keep the data.
app_first_boot_running409The setup this server was ordered with is still running. Wait until it finishes.
app_host_unsupported409This server's host can't install apps yet. Try again later or contact support.
app_install_busy409Another app is being installed on this server. Wait until it finishes.
app_install_running409The server is still reporting this install as running. Wait a few minutes after its last progress, then try again.
app_limit409This server holds as many apps as a server can.
app_no_credentials409There are no credentials to show yet. The server delivers them when the install finishes.
app_not_abandonable409That install has already finished or ended.
app_plan_too_small409This server's plan is too small for that app. Resize it first or choose a lighter app.
app_port_conflict409Another app on this server already uses a port this one needs.
app_rate_limited429Too many installs were requested in the last hour. Try again later.
app_restart_confirm_required409Installing an app restarts the server. Confirm that you want this.
app_server_not_active409This server is not active, so an app can't be installed on it.
app_server_not_running409The server must be running to install an app on it, and not in rescue mode or booted from an ISO. Start it and try again.
app_too_many_failures409Installs on this server have failed several times today. Contact support before trying again.
app_unsupported_os409This app installs on Debian and Ubuntu servers, and this server runs something else.
app_up_to_date409That app is already at the newest version Vexelia offers.
approval_not_open409This request has already been decided, or has expired.
approval_pending409This request is still waiting for someone else to approve it.
approval_required409This needs a second person's approval. Ask for it first.
approval_self403Someone else has to approve your request.
approval_stale409What this approval covered has changed since it was given. Ask again.
approval_unusable409This approval can't be used for this action. Ask again.
autopay_needs_card409Automatic renewal needs a saved card. Add a card, or turn automatic renewal off for these servers first.
backup_addon_required402Backups are an addon for this server. Turn the Backups addon on to back it up.
backup_cooldown429Manual backups are limited to 2 every 10 minutes per server. Try again shortly.
backup_daily_cap429This server has had as many backups today as its plan allows. Try again tomorrow.
backup_in_progress409A backup of this server is already running. Wait for it to finish.
backup_limit_reached409This plan's backup limit is reached. Delete a backup (unlock it first if it is locked) and try again.
backup_lock_limit409Too many backups are locked. Keep at least one slot free so new backups can replace the oldest one.
backup_locked409That backup is locked. Unlock it first.
backup_queued409A backup of this disk is already queued or running.
backup_state409That backup can't be changed in its current state. Wait for it to finish or fail.
backups_host_unsupported409The host running this server can't make or restore backups yet. Nothing was changed; try again later.
backups_not_included409This plan doesn't include backups.
bad_request400The request could not be read.
bad_signature400Webhook signature invalid.
block_in_use409This block still has addresses that are assigned, reserved or in quarantine, so it can't be deleted.
cancel_scheduled409This server is set to be cancelled. Undo the cancellation before protecting it.
cancel_unavailable409This server's paid period has already ended, so it can't be cancelled at the end of it. Pay the renewal to keep it, or destroy it now.
config_path_unknown404That file is not one of the config files this game lets you edit here.
conflict409That conflicts with existing data.
console_node_unreachable409The host running this server can't be reached right now. The server may still be running.
console_rebuilding409The server is being rebuilt. The console opens when it finishes.
console_server_stopped409The server is stopped. Start it to use the console.
console_server_suspended409The server is suspended, so its console is closed.
console_session_limit409This console is already open somewhere, or you have too many consoles open. Close one and try again.
console_unavailable503The console isn't available right now. Try again shortly.
contact_limit409An organisation can have at most 10 contacts.
contact_mail_limit429Too many confirmation emails have gone to that address or from this organisation today. Try again tomorrow.
contact_recently_sent409We sent a confirmation email a moment ago. Try again in a few minutes.
credentials_viewed409These credentials were already shown once and can't be shown again. If you didn't save them, reset the password on the server.
cross_origin403Cross-origin request refused.
dedicated_host_unsupported409This server's host is not ready for dedicated cores yet. Try again later or contact support.
deletion_blocked409This can't be deleted yet. The fields say what is in the way.
deletion_pending409A deletion is already waiting. Cancel it first, or let it run.
deletion_started409The deletion has started and can no longer be cancelled.
disk_busy409This disk is already being changed. Try again when that finishes.
disk_charge_open409Another change to this server's disks is waiting to be paid for or applied. Pay it or let it expire first.
disk_force_required409This server isn't running, so we can't check whether the disk is in use. Detach it with force if you are sure. If /etc/fstab still lists the disk without the nofail option, the server can fail to boot normally afterwards.
disk_limit409This server already has as many extra disks as its plan allows.
disk_retry409The disk can't be added just now. Your payment is kept and the disk is added as soon as it can be: try again in a minute.
disks_not_offered409This server's plan has no extra disks.
document_not_open409That is no longer waiting for payment.
downgrades_disabled409Downgrades can't be scheduled right now. Contact support.
email_change_failed409That address can't be used. Try another, or contact support.
email_taken409That email can't be used.
email_unverified403Confirm your email address first. We can resend the link.
eula_required409Accept the game's current EULA before starting this server.
eula_unavailable409No EULA is published for this game yet, so it can't be started.
eula_version_changed409The EULA changed. Read the current text and accept it.
event_not_dead409Only a payment event that ran out of attempts can be marked handled.
event_processed409This payment event has already been processed.
export_not_ready409That export is not ready, or its download time has ended. Ask for a new one.
export_open409An export is already being prepared. Wait for it to finish, then ask again.
ext_hook_disabled409This hook is switched off. Switch it on first.
ext_hook_limit409The platform already has the most hooks it holds. Delete one first.
ext_hook_replay_changed409The deliveries this replay would send are not the ones that were previewed or approved. Preview it again.
ext_hook_replay_limit409A replay can carry at most 200 deliveries and must fit in the hook's queue of 1000. Narrow the filter, or wait for the queue to drain.
ext_hook_replay_needs_approval409Replaying this many deliveries needs a second person's approval.
ext_override_languages409The most languages are already in use. Remove one first.
ext_override_no_draft409There is no draft to publish. Save one first.
ext_override_render409This text could not be shown with the sample data.
ext_override_stale409Someone else saved this draft after you opened it. Reload to see their version.
ext_pane_limit409The platform already holds the most manifests it takes.
ext_pane_no_keys409No manifest signing key is enrolled on this platform, so no manifest can be uploaded.
ext_pane_state409This manifest is not in a state that allows that.
ext_pane_version409A manifest with this id has this version or a higher one already. Upload a higher version.
ext_plugin_disabled409This plugin is switched off.
ext_plugin_limit409The platform already holds the most plugins or waiting uploads it allows. Uninstall or discard one first.
ext_plugin_no_keys409No plugin signing key is enrolled on this platform (VEXELIA_EXT_PLUGIN_PUBKEYS), so no plugin can be installed or run.
ext_plugin_signature422The package is not signed by a key enrolled on this platform, or it was changed after signing.
ext_plugin_version_not_newer409A plugin with that id is installed, or was installed, at this version or a newer one. Upload a higher version.
ext_plugins_disabled409Plugins are switched off on this platform (feature flag ext_plugins_enabled). Turning one off, discarding an upload and uninstalling still work.
ext_template_locked409This email can't be overridden. Its wording is fixed.
ext_upload_changed409The upload is not the one this request names. Check the plugin id, version and manifest hash against the upload.
ext_upload_expired409This upload waited too long for approval and lapsed. Upload the package again.
file_conflict409The file changed since you opened it. Reload it before saving.
file_denied403The game manages that file itself, so it can't be changed here.
file_disk_full409There isn't enough disk space left on this server for that. Free some space or upgrade the plan.
file_exists409Something with that name already exists.
file_not_found404That file or folder doesn't exist.
file_path_invalid422That path isn't allowed. Use plain folder and file names without .. or special characters.
file_special409Links and device files can't be opened here. You can rename or delete them.
file_too_large413That is too large for the file manager (2 MB to move, 1 MB to edit). Use SFTP for big files.
file_wrong_type409That is a folder where a file was expected, or a file where a folder was expected.
files_busy429Too many file requests at once. Wait a moment and try again.
files_failed502The host could not complete that file request. Try again; if it keeps failing, contact support.
files_node_unreachable409The host running this server can't be reached right now. Your files are safe. Try again shortly.
files_node_unsupported409The host running this server is not ready for the file manager yet. Use SFTP later, or contact support.
files_server_suspended409The server is suspended, so its files are closed.
files_too_many413That folder holds too many items to handle at once.
files_unavailable503File access isn't available right now. Try again shortly.
firewall_changed409This server's firewall changed while that ran. Try again.
firewall_group_in_use409This rule group is used by firewall policies. Remove it from them first.
firewall_in_use409This firewall policy is attached to servers. Detach it from them first.
firewall_limit409You have reached the limit of firewall policies or rules.
firewall_lockout409This change would block your own connection to the server. Apply it anyway to confirm.
firewall_no_test409This server's firewall is not in test mode.
firewall_test_over409The test ended and the previous firewall was put back. Apply the change again to test it.
firewall_too_many_rules409That would give a firewall policy more rules than it can hold. Rules from its groups count too.
firewall_version_conflict409Someone else changed this firewall. Reload it and make the change again.
floating_ip_assigned409That floating IP is assigned to a server. Unassign it first.
floating_ip_billing_ended409The server that paid for this floating IP has ended, so it can't be assigned. Release it, or contact support to move its billing.
floating_ip_busy409That floating IP is changing (assigning, unassigning or being released). Try again when it finishes.
floating_ip_charge_open409Another payment for a volume or floating IP is waiting or being applied. Pay it, or wait for it to finish or expire, then try again.
floating_ip_confirm_required409Taking a floating IP off a server stops traffic to that address at once. Confirm that you want this.
floating_ip_host_unsupported409This server's host is not ready for floating IPs yet. Try again later or contact support.
floating_ip_limit409This organisation has reached its limit of floating IPs. Release one first, or ask support to raise the limit.
floating_ip_name_taken409A floating IP of this organisation already has that name. Choose another.
floating_ip_no_pool409There are no floating IP addresses available in this server's location right now. Try again later or contact support.
floating_ip_not_assigned409That floating IP is not assigned to a server.
floating_ip_other_group409That floating IP belongs to a different location from this server. A floating IP can be assigned only to servers in its own location.
floating_ip_other_node409That floating IP stays on the host it was first assigned on. Choose a server on that host.
floating_ip_server_full409This server already holds as many floating IPs as a server can.
folder_not_empty409That folder isn't empty. Delete it with its contents to remove it.
forbidden403You don't have permission to do that.
game_address_taken409Another node already has that public address.
game_image_exists409That image reference is already registered.
game_image_key_missing409No game image signing key is configured, so no image can be registered.
game_image_registry422That image is not from an allowed registry.
game_image_signature422The signature does not verify for this image reference with the game image key.
game_image_state409The image is not in a state that allows that.
game_network_host_unsupported409The host for this game server can't give it a network address yet. Nothing was changed; try again later.
game_network_in_use409Servers on this node hold its address or ports. Move them off first, or widen the range instead.
game_unavailable409That game can't be ordered right now. Nothing was charged.
grant_escalation403You can only give rights you hold yourself.
idempotency_in_progress409A request with this Idempotency-Key is still running. Try again shortly.
idempotency_key_reused422This Idempotency-Key was already used for a different request. Use a new key for a new request.
idempotency_outcome_unknown409An earlier request with this Idempotency-Key failed on our side and may have run. Check the resource, or wait 15 minutes and send the request again.
idempotency_replay_unavailable409This Idempotency-Key was used for a request that succeeded, but its answer was too large to keep. Read the resource to see the result.
image_revoked409The game image this server runs from was withdrawn, so it can't be started. Contact support.
impersonation_forbidden403This is not available while viewing an account.
impersonation_in_progress409You are already starting a viewing session. Try again.
impersonation_read_only403You are viewing this account read-only. Nothing can be changed.
internal500Something went wrong on our side.
invalid_code401That code isn't valid.
invalid_credentials401Email or password is incorrect.
invite_invalid404This invitation is not for your account, or it has expired or was withdrawn.
invite_inviter_changed409The person who invited you can no longer give you that role. Ask them for a new invitation.
invoice_not_creditable409This invoice cannot be credited this way.
ip_reassigned409One of this server's addresses has been given to someone else, so it can't be restored.
iso_fetch_queue_full429Three ISO downloads are already waiting. Try again when one has finished.
iso_hash_mismatch422The ISO does not match the SHA-256 you gave.
iso_in_use409This ISO is mounted on a server. Unmount it first.
iso_not_iso422That file is not an ISO 9660 or UDF disc image.
iso_quota_exceeded409This would take your ISO library over its quota. Delete an ISO you no longer need first.
iso_size_mismatch422The upload was not the size it said it would be. Try again.
iso_too_large413That ISO is larger than the 10 GiB allowed.
iso_unavailable503The ISO library is not available right now. Try again later.
iso_upload_format415Send the ISO as the raw request body with Content-Type: application/octet-stream and a Content-Length.
job_not_idempotent409This job is not safe to run on demand. It runs on its own schedule.
last_owner409An organisation needs at least one owner.
last_staff_owner409This is the last owner. Make someone else an owner first.
limit_reached409Your organisation has reached a limit. You can ask for a higher one under Account, Limits.
limit_request_closed409This request has already been answered.
limit_request_open409You already have an open request for this limit. We will answer it soon.
lock_not_allowed409That account can't be locked from here: it is your own, a staff account, or not an active account.
mail_not_retryable409Only an email that failed, ran out of retries or has an unknown result can be sent again.
mail_secret_bearing409This email carries a single-use link, so it is never sent again. Ask the customer to request it again (verify, reset or invite).
member_outranks403That member can do more than you can, so you can't change them.
mod_checksum_mismatch502The download did not match the checksum this game's list pins, so nothing was installed.
mod_fetch_failed502We could not download that file just now. Try again in a few minutes.
mod_file_present409A different file with that name is already in the mods folder. Remove or rename it first.
mod_not_installed409That mod is not installed from this list.
mod_unknown404That mod is not on this game's list.
no_capacity409That region is full right now. Try another region or a smaller plan.
no_firewall_reset409This server has no staff firewall reset to restore.
node_full409The node holding this server's disk has no room for it right now.
node_unavailable409The node holding this server's disk isn't active, so it can't be restored now.
not_cancelling409This server isn't set to cancel, or its paid period has already ended.
not_found404Not found.
not_in_rescue409This server isn't in rescue mode.
not_quarantined409That address isn't in quarantine.
not_text_file415That isn't a plain text file, so it can't be edited here. Download it instead.
offsite_unavailable409Off-site copies are not available yet: no off-site provider is connected.
op_not_abortable409Only an operation that hasn't been sent to the node yet can be aborted.
op_not_resolvable409Only the latest operation that needs review can be resolved by hand.
op_not_retryable409This operation can't be retried from its current state.
org_erasing409This organisation is being deleted, so nothing can be changed in it.
paid_period_ended409This server's paid period has ended or is not the current one. Renew it first, then change its disks.
password_breached422That password has appeared in a known data breach. Choose a different one.
payment_disputed409That payment is under a card dispute, so nothing can be refunded or credited from it until the dispute is decided.
payment_unavailable409This payment can't be completed online. Contact support.
paypal_declined409PayPal declined this payment. Nothing was charged. Try another way to pay.
paypal_not_approved409PayPal hasn't confirmed your approval of this payment yet. Go back to PayPal to approve it, then try again.
paypal_unavailable503PayPal isn't responding right now. Nothing was charged. Try again in a minute.
period_closed409That period is closed. A correction is a reversal entry dated now, not an edit.
period_not_ready409That period can't be closed yet.
personal_org409Personal organisations can't be changed this way.
plan_change_open409A plan change for this server is waiting to be paid for or applied. Pay it or let it expire first.
plan_disk_shrink409A disk can't shrink. To move to a plan with a smaller disk, rebuild the server onto it.
plan_in_use409Servers or open orders still use this plan, so its specifications can't change. Create a new plan and retire this one.
plan_no_room409The host this server runs on doesn't have room for that plan right now. Try a smaller plan, or try again later.
plan_other_arch409That plan runs on a different processor type (x86 or Arm) than this server. A server never changes it: order a new server on that plan and move your data.
plan_other_class409That plan has a different kind of CPU (shared or dedicated) than this server. A server never changes it: order a new server on that plan and move your data.
plan_too_late409Too little of the paid period is left to charge for an upgrade. Renew first, then change plan.
private_network_address_taken409Another server on that private network has that address.
private_network_already_member409This server is on that private network already.
private_network_busy409That server's interface on the private network is changing. Try again when it finishes.
private_network_confirm_required409Taking a server off a private network stops its traffic on that network at once. Confirm that you want this.
private_network_firewall_ack_required409This server's firewall policy filters its public interface only, not its private network interface. Confirm that you understand this to attach it.
private_network_full409That private network holds as many servers as a network can.
private_network_host_unsupported409This server's host is not ready for private networks yet. Try again later or contact support.
private_network_in_use409Servers are still attached to that private network. Take them off it first.
private_network_limit409This organisation has reached its limit of private networks. Delete one first, or ask support to raise the limit.
private_network_name_taken409A private network of this organisation already has that name. Choose another.
private_network_no_room409That private network has no free address left.
private_network_not_member409This server is not on that private network.
private_network_other_group409That private network is in a different location from this server. A server can be attached only to networks in its own location.
private_network_other_node409That private network is on a different host from this server. Servers on one network must run on the same host for now.
private_network_overlap409That range overlaps another private network of this organisation. Choose a range that does not.
private_network_server_full409This server is on as many private networks as a server can be.
proforma_not_payable409This renewal has already been paid or is no longer due.
purchase_blocked403Purchases are blocked for this organisation right now. Contact support.
quarantine_abuse409This address is in quarantine because of abuse, so it can't be released early.
quote_expired409The price you were shown has expired. Ask for a new one.
rate_limited429Too many requests. Try again shortly.
rdns_lookup_failed503We could not look that name up just now. Try again in a minute.
rdns_not_confirmed422That name doesn't point to this address yet, so reverse DNS can't be set to it.
rdns_settled409This request has already been answered.
rebuild_unavailable409This server can't be rebuilt in its current state. Only an active server can be rebuilt.
reference_already_recorded409That bank reference was already recorded for this method.
refund_not_failed409This refund hasn't failed, or has already been dealt with another way.
reinstall_needs_stopped409Stop the server before reinstalling it.
reinstall_unavailable409This server's host can't reinstall it yet. Nothing was changed; try again later.
renewal_charging409This server's renewal is being charged. Try again in a few minutes.
renewal_unpaid409This server's renewal is unpaid. Place a hold first, or ask the customer to pay.
request_declined403We can't complete this request. If you think this is a mistake, contact support.
request_declined_limit409This request goes over a limit on your account. Contact support if you need it raised.
request_declined_region403This isn't available for your region right now.
rescue_unavailable409Rescue mode isn't available for this server yet.
restore_expired409The 72-hour trash period for this server has ended.
restore_in_progress409A restore of this server is already running.
restore_needs_stopped409Stop the server before restoring a backup.
restore_no_addresses409No addresses were recorded for this server when it was destroyed, so it can't be restored.
restore_not_allowed409This server has nothing in trash to restore: its setup didn't finish, it was refunded, or its disk was already gone.
restore_open409A restore onto this server is already waiting or running.
restore_overdue402This server is past its paid period. Pay the open invoice before restoring a backup.
role_builtin409Built-in roles can't be changed.
role_escalation403You can only give a role whose permissions you hold yourself.
role_exists409A role with that name already exists.
role_frozen409legacy_staff is frozen. It can be taken away but never given.
role_in_use409People still hold this role. Take it away from them first.
role_limit409An organisation can have at most 20 roles of its own.
role_owner_cli409The owner role is only given with the bootstrap-owner command, never from the panel.
schedule_limit_reached409This plan's schedule limit is reached. Delete a schedule and try again.
schedule_name_taken409A schedule with that name already exists on this server.
schedule_task_forbidden403You don't have permission to schedule that kind of task.
schedules_not_included409This plan doesn't include schedules.
second_factor_needed401Enter your two-factor code to continue.
server_busy409This server is busy with another action. Try again when it finishes.
server_not_active409This server can't do that in its current state.
server_not_stopped409Stop the server first. A firmware change applies when it starts again.
server_protected409This server is protected from being destroyed. Turn the protection off first.
service_held409A hold protects this server. Lift it first.
sftp_unavailable503SFTP isn't available right now.
snapshot_busy409That snapshot is being taken, restored or deleted. Try again when it finishes.
snapshot_charge_open409A payment for a snapshot is waiting. Pay it or let it expire first.
snapshot_limit409This organisation has reached its limit of snapshots. Delete one first, or ask support to raise the limit.
snapshot_name_taken409A snapshot of this server already has that name. Choose another.
snapshot_server_changed409This server's disk has changed since the snapshot was taken (it was rebuilt, restored from a backup or resized), so the snapshot can't be restored onto it.
snapshot_server_limit409This server has reached its limit of snapshots. Delete one first to take another.
sso_already_linked409Your account is already linked to that provider. Unlink it first to link a different identity.
sso_last_sign_in_method409That is the only way you can sign in. Set a password or add a passkey first.
sso_not_for_staff403Staff accounts sign in with a password and a second factor, not with an identity provider.
sso_unavailable503Sign-in with that provider is not available right now. Use your password, or try again later.
step_up_required403Confirm it's you to continue.
subuser_exists409That person already has access to this server, or an invitation is waiting for them.
subuser_limit_reached409This server has as many invitations and guests as it can have. Revoke one and try again.
support_grant_active409Support already has access to this server. Withdraw it first to give different access.
tax_unavailable503We can't work out the tax for this right now. Please try again later, or contact support if it keeps happening.
template_eula_missing409No EULA text is published for this game. Publication is blocked.
template_id_mismatch422The template's id is not the one in the address.
template_licence_mismatch409The template's licence block does not match the owner's recorded licence check.
template_licence_missing409No licence check has been recorded for this template by the owner, or the template does not carry it. Publication is blocked.
template_signature422The signature does not verify for this template and version with a known key.
template_version_exists409That version of the template is already published. Versions are immutable; publish a new version.
template_version_older409A newer version of this template is already published.
ticket_closed409This ticket was closed too long ago to reopen. Open a new ticket and mention this one.
ticket_limit409This organisation has too many open tickets, or this ticket is too long. Close one first, or open a new ticket.
token_invalid400This link is invalid or has expired.
token_limit409This organisation has reached its limit of active API tokens. Revoke one first.
topup_not_payable409This top-up expired or was cancelled, so it can't be paid. Start a new top-up.
transfer_invalid409This ownership transfer can no longer be accepted.
transfer_open409An ownership transfer is already waiting for an answer. Cancel it to start another.
trial_needs_card409Add a card to your account first. A trial needs one on file; nothing is charged for the trial.
trial_not_eligible409This account isn't eligible for a free trial.
trial_open409This account already has a free trial.
trial_server409This server is a free trial, so it has no paid changes until you keep it. Keep it first, or end the trial.
trial_state409The trial can't do that in its current state.
trial_unavailable409Free trials aren't available right now.
two_factor_enrolment_required403Turn on two-factor sign-in to continue.
two_factor_not_enrolled409Turn on two-factor sign-in on your own account first, so you are not locked out.
two_factor_required_by_policy409Your organisation or your staff role requires a second factor, so the last one can't be removed.
two_factor_signin_required403Sign in with your existing second factor before adding another.
unauthenticated401Sign in to continue.
unsupported_media415Send JSON with Content-Type: application/json.
validation_failed422Some fields need attention.
vat_check_rate_limited429You have asked for the VAT number to be checked several times in the last hour. We keep trying automatically; look again later.
vat_check_stale409That VAT check is no longer the current one: the tax ID, the check or its approval state changed. Reload and review it again.
volume_attached409That volume is attached to a server. Detach it first.
volume_busy409That volume is changing (attaching, detaching, resizing or being deleted). Try again when it finishes.
volume_charge_open409Another payment for a volume or floating IP is waiting or being applied. Pay it, or wait for it to finish or expire, then try again.
volume_host_unsupported409This server's host is not ready for volumes yet. Try again later or contact support.
volume_limit409This organisation has reached its limit of volumes. Delete one first, or ask support to raise the limit.
volume_name_taken409A volume of this organisation already has that name. Choose another.
volume_no_slot409This server has no free extra-disk slot: its four slots are used by disks and volumes.
volume_not_attached409That volume is not attached to this server.
volume_other_node409That volume's data is on a different host from this server. A volume can be attached only to servers on the host that holds it.
weak_password422Use at least 10 characters.
webhook_disabled409This webhook endpoint is switched off. Switch it on first.
webhook_limit409This organisation has reached its limit of webhook endpoints. Delete one first, or ask support to raise the limit.

api-tokens

GET /api/v1/orgs/{org}/api-tokens

List API tokens

Every token of the organisation, newest first, with its scopes, limits and use. Includes the scope catalog a token can be given. Owners and admins only, and only with a signed-in session, never with a token.

  • Permission: api_tokens.manage
  • API token: not available, use a signed-in session
ResponseMeaning
200Tokens
defaultError

POST /api/v1/orgs/{org}/api-tokens

Create an API token

Needs step-up. The response carries the token once; it is stored only as a hash and can never be shown again. Scopes must be within your own role. A token with servers:rebuild or servers:destroy needs an IP allowlist or an expiry under 30 days (422 on expires_at otherwise). At most 50 active tokens per organisation (token_limit).

  • Permission: api_tokens.manage
  • API token: not available, use a signed-in session
  • Step-up: needs a recent password check

Request body (JSON):

{
  "allowed_ips": [
    null
  ],
  "expires_at": "2026-01-01T00:00:00Z",
  "name": "string",
  "scopes": [
    null
  ]
}
ResponseMeaning
201Created; the token is shown only here
defaultError

DELETE /api/v1/orgs/{org}/api-tokens/{token}

Revoke an API token

The token stops working on its very next request. Revoking a token that is already revoked is not an error.

  • Permission: api_tokens.manage
  • API token: not available, use a signed-in session
ResponseMeaning
204Done
defaultError

audit

GET /api/v1/orgs/{org}/audit

Organisation activity

Lists activity in the organisation, newest first. Staff actions are shown, but not who took them. Pages with before and limit (default 50, at most 100).

  • Permission: audit.read
  • API token: not available, use a signed-in session
ResponseMeaning
200Events
defaultError

auth

POST /api/v1/auth/email-change/cancel

Cancel a change of sign-in address from the link sent to the old address

Cancels a waiting change with the token from the notice sent to the current address. Works once; an unknown, used or expired token returns token_invalid.

  • Access: public, no authentication

Request body (JSON):

{
  "token": "string"
}
ResponseMeaning
200Cancelled
defaultError

POST /api/v1/auth/email-change/confirm

Confirm the new sign-in address from the emailed link

Makes the change with the token from the link sent to the new address. The new address becomes the sign-in address and counts as verified, every session of the account is signed out and unused email links are voided. An unknown, used, cancelled or expired token, a locked account, or a request made from an address the account no longer has returns token_invalid; an address that has since been taken returns email_change_failed.

  • Access: public, no authentication

Request body (JSON):

{
  "token": "string"
}
ResponseMeaning
200Changed
defaultError

POST /api/v1/auth/login

Sign in with email and password

Signs in with email and password. Returns signed_in, or second_factor with the allowed methods when the account has an authenticator app, in which case only a partial session is set until the second step. Wrong credentials, inactive accounts and rate limiting are refused; email_unverified and account_locked are shown only after the password matched. A browser the person marked as trusted skips the second step and gets signed_in (only when it is still valid, was marked for this person and this remembered browser, comes from a network they have signed in from, and no automatic lock is running; never for staff or for a person in an organisation that requires a second factor); ask_second_factor forces the step. Several failed sign-ins in 15 minutes start an automatic temporary lock: sign-in from a browser or network the person has not used is then refused for a few minutes with the same invalid_credentials answer a wrong password gets (right or wrong password, no difference), and the person is emailed.

  • Access: public, no authentication

Request body (JSON):

{
  "ask_second_factor": false,
  "email": "string",
  "password": "string"
}
ResponseMeaning
200Result
defaultError

POST /api/v1/auth/login/second-factor

Complete sign-in with a TOTP or recovery code

Completes sign-in with a TOTP code or a recovery code. Needs the partial session from login; any other session gets bad_request. Wrong codes are refused and count towards the automatic temporary lock. With trust_browser and an authenticator code the browser is marked as trusted for the configured number of days (a cookie only the sign-in routes receive; the person is emailed); a recovery code never marks one.

  • API token: not available, use a signed-in session

Request body (JSON):

{
  "code": "string",
  "recovery_code": "string",
  "trust_browser": false
}
ResponseMeaning
200Signed in
defaultError

POST /api/v1/auth/logout

Sign out

Ends the current session and clears the session cookie.

  • API token: not available, use a signed-in session
ResponseMeaning
204Done
defaultError

POST /api/v1/auth/passkey/begin

Start passkey sign-in

Starts passkey sign-in and returns the WebAuthn options with a challenge_id. Pass the challenge_id to passkey/finish.

  • Access: public, no authentication
ResponseMeaning
200Options
defaultError

POST /api/v1/auth/passkey/finish

Finish passkey sign-in

Completes passkey sign-in with the browser's WebAuthn response and the challenge_id from passkey/begin. A challenge is single-use. Any failure, including an unverified or inactive account, returns invalid_credentials.

  • Access: public, no authentication
ParameterInNotes
X-Vexelia-Challengeheader

Request body (JSON):

{}
ResponseMeaning
200Signed in
defaultError

POST /api/v1/auth/password/forgot

Email a password reset link

Emails a password reset link if the address belongs to an active account. The response is the same either way, so it does not reveal whether an account exists.

  • Access: public, no authentication

Request body (JSON):

{
  "email": "string"
}
ResponseMeaning
202Check email
defaultError

POST /api/v1/auth/password/reset

Set a new password from a reset link

Sets a new password using the token from a reset email and sends a notice that the password changed. The new password must meet the password policy (weak_password: length) and must not be a very common password or one found in a known breach (password_breached; the breach-list lookup is a platform setting and lets the password through when that service is down); an unknown or used token returns token_invalid.

  • Access: public, no authentication

Request body (JSON):

{
  "password": "string",
  "token": "string"
}
ResponseMeaning
200Reset
defaultError

POST /api/v1/auth/register

Create an account

Creates an account and a personal organisation, and emails a confirmation link. The password must be 10 to 256 characters and must not be a very common password or one found in a known breach (validation_failed on the password field; the same answer whether or not the address is registered). The response is the same for an address that is already registered, whose owner is told by email instead. When the platform runs a sync hook plugin on signups the request can be refused with 403 request_declined, 409 request_declined_limit or 403 request_declined_region, in the platform's own words and before anything is created; the answer never depends on whether the address has an account. A plugin that fails or is slow is ignored.

  • Access: public, no authentication

Request body (JSON):

{
  "email": "string",
  "name": "string",
  "password": "string"
}
ResponseMeaning
202Check email
defaultError

POST /api/v1/auth/secure-account

Sign out everywhere from a sign-in alert (this wasn't me)

The link in a new-device or new-network sign-in alert. Signs every session of the account out, forgets the browser and network the alert was about (the next sign-in from them alerts again) and emails a password reset link to the account's address. It does not lock the account and leaves API tokens, passkeys and the authenticator app as they are. The link works once and for seven days; an unknown, used or expired token returns token_invalid.

  • Access: public, no authentication

Request body (JSON):

{
  "token": "string"
}
ResponseMeaning
200Signed out everywhere
defaultError

GET /api/v1/auth/sso/providers

Identity providers a visitor can sign in with

The key and name of each provider a visitor can use, for the sign-in page. Public. Empty while the platform flag sso_enabled is off or no provider is configured, so the page shows no button and nothing can be learned about what exists. It never names an issuer, a client id or a secret.

  • Access: public, no authentication
ResponseMeaning
200The providers
defaultError

POST /api/v1/auth/sso/{provider}/begin

Start signing in with an identity provider

Starts the authorization code flow with PKCE (S256), a state and a nonce, and returns the provider's authorization address. The state is single-use, kept only as a hash, lives ten minutes and is tied to this browser by an HttpOnly cookie (vx_sso), so the callback must come back to the same browser. The PKCE verifier and the nonce are derived from the state with a server key and never stored. return_to is kept only if it is a path on this site. Public. A provider that is unknown or switched off answers not_found; one that cannot be reached answers sso_unavailable and leaves nothing behind.

  • Access: public, no authentication

Request body (JSON):

{
  "return_to": "string"
}
ResponseMeaning
200Where to send the browser
defaultError

GET /api/v1/auth/sso/{provider}/callback

Where the identity provider sends the browser back

The redirect address registered with the provider (exact, one per provider). Public: the single-use state, the browser cookie and the signed id_token are the credentials. Every outcome is a 303 redirect to the panel, never a JSON error. Success lands on the return_to of the request, or on /login?sso=second_factor when an authenticator app is still to be asked for. A refusal lands on /login?sso_error=CODE (or /account/security?sso_error=CODE for a link) with CODE one of sso_failed, sso_cancelled, sso_unavailable, sso_not_linked, sso_link_failed or account_locked; every failure to sign in reads sso_failed or sso_not_linked whatever accounts exist, so nothing says whether an address or identity has an account. The id_token must be signed RS256, PS256, ES256 or EdDSA by a key in the provider's published key set and carry the expected issuer, audience, nonce and a future expiry, and a verified email (on the provider's domain allow-list if it has one). An identity signs in only to the account it was linked to: never by email match, never for staff.

  • Access: public, no authentication
ParameterInNotes
codequery
statequery
issqueryThe provider's issuer (RFC 9207); must equal the configured one.
errorquery
ResponseMeaning
303Redirect to the panel
defaultError

POST /api/v1/auth/verify-email

Confirm an email address

Confirms an email address with the token from the confirmation email. An unknown, used or expired token returns token_invalid.

  • Access: public, no authentication

Request body (JSON):

{
  "token": "string"
}
ResponseMeaning
200Verified
defaultError

POST /api/v1/auth/verify-email/resend

Resend the confirmation email

Sends the confirmation email again. The response is the same whether or not the address exists.

  • Access: public, no authentication

Request body (JSON):

{
  "email": "string"
}
ResponseMeaning
202Check email
defaultError

billing

GET /api/v1/orgs/{org}/billing

Credit balance and saved cards

Returns the organisation's spendable credit balance (balance minus holds) and its saved cards.

  • Permission: billing.read
  • API token: not available, use a signed-in session
ResponseMeaning
200Billing
defaultError

billing-profile

GET /api/v1/orgs/{org}/billing-profile

Get billing details

An org that never saved details gets empty strings and a null updated_at.

  • Permission: billing.read
  • API token: not available, use a signed-in session
ResponseMeaning
200The billing address and tax ID printed on new invoices
defaultError

PUT /api/v1/orgs/{org}/billing-profile

Save billing details

Replaces every field. Only invoices issued afterwards carry the new details; an issued invoice keeps the copy it was issued with. The tax ID is free text and is not checked against a registry.

  • Permission: billing.manage
  • API token: not available, use a signed-in session

Request body (JSON):

{
  "address": "string",
  "city": "string",
  "country": "string",
  "name": "string",
  "postal_code": "string",
  "tax_id": "string"
}
ResponseMeaning
200The saved details
defaultError

GET /api/v1/orgs/{org}/billing-profile/vat-check

Show whether the VAT number was validated

The state of the EU VAT number saved on the billing details (TAX-2). It reads what is stored and never calls the registry. `status` is none (no tax ID), not_eu (not an EU VAT number, nothing to check), unmatched (the registry knows the number but its registered name did not match the billing name and country; under staff review, no reverse charge), off (validation is switched off for this organisation), pending (not yet answered), valid, invalid, or unverified (the registry could not answer, or a valid answer is too old to rely on; a retry is queued). `reverse_charge_applies` is true only for a valid answer inside the trust window. The VAT number itself is not repeated.

  • Permission: billing.read
  • API token: not available, use a signed-in session
ResponseMeaning
200The validation state
defaultError

POST /api/v1/orgs/{org}/billing-profile/vat-check

Validate the VAT number now

Asks the registry about the saved VAT number once (at most one call per number every 30 seconds; a fresh answer is returned from storage). A registry outage is not an error: the answer is 200 with status `unverified`, the number stays accepted as entered and the worker retries with backoff. 404 while validation is switched off for the organisation; 429 `vat_check_rate_limited` after 5 checks an hour per organisation; 422 on `tax_id` when the saved tax ID is not an EU VAT number. Never blocks saving details or checkout.

  • Permission: billing.manage
  • API token: not available, use a signed-in session
ResponseMeaning
200The validation state after the check
defaultError

catalog

GET /api/v1/catalog

Regions, plans, prices and images

Lists what can be ordered: active regions, plans with their current prices, operating-system images, the billing currency and the payment provider's public key. Hidden and retired plans are not shown.

  • API token: not available, use a signed-in session
ResponseMeaning
200Catalog
defaultError

contacts

POST /api/v1/contacts/verify

Confirm a contact address

Confirms a contact with the token from the confirmation email. The person it was mailed to needs no account. An unknown, used or expired token returns token_invalid.

  • Access: public, no authentication

Request body (JSON):

{
  "token": "string"
}
ResponseMeaning
200Confirmed
defaultError

GET /api/v1/orgs/{org}/contacts

List contacts

Lists the organisation's contacts: addresses it wants told about billing, technical or security notices, with whether each has been confirmed.

  • Permission: org.members.read
  • API token: not available, use a signed-in session
ResponseMeaning
200Contacts
defaultError

POST /api/v1/orgs/{org}/contacts

Add a contact

Adds an address for billing, technical and/or security notices and emails it a confirmation link that works for 7 days. Nothing is sent to the address for the organisation until its owner confirms it. An address can be a contact once per organisation (409), and an organisation has at most 10 (409 contact_limit). Confirmation mail is limited: a re-add of the same address within five minutes is a resend (409 contact_recently_sent), one address gets at most 3 a day from all organisations and an organisation sends at most 20 a day, 5 for a personal one (429 contact_mail_limit).

  • Permission: org.manage
  • API token: not available, use a signed-in session

Request body (JSON):

{
  "email": "string",
  "kinds": [
    null
  ],
  "name": "string"
}
ResponseMeaning
201Added
defaultError

DELETE /api/v1/orgs/{org}/contacts/{contact}

Remove a contact

Removes a contact. It gets no further mail and its address is deleted.

  • Permission: org.manage
  • API token: not available, use a signed-in session
ResponseMeaning
204Done
defaultError

PATCH /api/v1/orgs/{org}/contacts/{contact}

Change a contact

Changes a contact's name and which kinds of notice it gets. The address itself cannot change: add a new contact, which its owner confirms. Narrowing the kinds keeps the confirmation; adding a kind resets it and mails a new link (subject to the same limits as adding).

  • Permission: org.manage
  • API token: not available, use a signed-in session

Request body (JSON):

{
  "kinds": [
    null
  ],
  "name": "string"
}
ResponseMeaning
200Changed
defaultError

POST /api/v1/orgs/{org}/contacts/{contact}/resend

Send the contact's confirmation again

Mails a new confirmation link to an unconfirmed contact; the old link stops working. At most once in five minutes (409 contact_recently_sent). A confirmed contact answers 409.

  • Permission: org.manage
  • API token: not available, use a signed-in session
ResponseMeaning
204Done
defaultError

credit-notes

GET /api/v1/orgs/{org}/credit-notes

List the organisation's credit notes

Newest first. A credit note reverses part or all of one invoice and says where the money went (the account credit, or the card it was paid with and the state of that refund). The staff reason is not shown.

  • Permission: billing.read
  • API token: not available, use a signed-in session
ParameterInNotes
beforequeryThe next_before value of the previous page
limitquery
ResponseMeaning
200Credit notes
defaultError

GET /api/v1/orgs/{org}/credit-notes/{note}.pdf

Download a credit note as PDF

Rendered only from the issued credit note and the frozen snapshot of the invoice it reverses. Another organisation's credit note is a 404.

  • Permission: billing.read
  • API token: not available, use a signed-in session
ResponseMeaning
200The credit note as a PDF attachment
defaultError

deletion

DELETE /api/v1/orgs/{org}/deletion

Cancel a request to delete the organisation

An owner only. Stops a pending request; the owners and admins are emailed.

  • Permission: org.manage
  • API token: not available, use a signed-in session
  • Session only: a person must be present
ResponseMeaning
204Done
defaultError

GET /api/v1/orgs/{org}/deletion

Whether the organisation can be deleted, and any request

As for your account, for this organisation. `allowed` is also false unless you are an owner.

  • Permission: org.manage
  • API token: not available, use a signed-in session
  • Session only: a person must be present
ResponseMeaning
200The state
defaultError

POST /api/v1/orgs/{org}/deletion

Ask for the organisation to be deleted

An owner only (403 otherwise), never a personal organisation (personal_org). Confirm with the organisation's name. Refused with 409 deletion_blocked while servers, money, disputes or holds are in the way. The owners and admins are emailed; after the grace period the organisation's members, SSH keys, tokens, webhooks and variables are deleted and its tickets and billing contact are anonymised, and its invoices and ledger stay. Needs a recent step-up and a panel session.

  • Permission: org.manage
  • API token: not available, use a signed-in session
  • Step-up: needs a recent password check
  • Session only: a person must be present

Request body (JSON):

{
  "confirm": "string"
}
ResponseMeaning
201The request
defaultError

deposits

GET /api/v1/orgs/{org}/deposits

List recent top-ups

Lists recent account-credit top-ups and the minimum and maximum amount allowed for a new one.

  • Permission: billing.read
  • API token: not available, use a signed-in session
ResponseMeaning
200Recent top-ups, newest first, with the allowed amount range
defaultError

POST /api/v1/orgs/{org}/deposits

Add account credit

Creates a top-up and returns the card payment to confirm. Send a fresh Idempotency-Key per top-up: repeating it with the same amount returns the same top-up; with another amount it is a conflict.

  • Permission: billing.manage
  • API token: not available, use a signed-in session
ParameterInNotes
Idempotency-Keyheader

Request body (JSON):

{
  "amount_minor": 0,
  "currency": "USD"
}
ResponseMeaning
201Top-up created; pay with client_secret
defaultError

GET /api/v1/orgs/{org}/deposits/options

Ways to add account credit

Says which ways of adding account credit this organisation is offered. Card is always listed. PayPal is true only when staff have stored PayPal credentials and switched the paypal_enabled flag on for this organisation; until then the PayPal routes answer 404 as if they did not exist.

  • Permission: billing.read
  • API token: not available, use a signed-in session
ResponseMeaning
200The offered ways to pay
defaultError

POST /api/v1/orgs/{org}/deposits/paypal

Add account credit with PayPal

Creates a top-up paid with PayPal and returns the PayPal address the customer approves it at. Nothing is charged yet: after approval the customer returns to the panel, which calls the capture operation. Send a fresh Idempotency-Key per top-up (up to 90 characters): repeating it with the same amount returns the same top-up and the same PayPal order; with another amount it is a conflict. The amount limits and the US dollar currency are those of a card top-up. Answers 404 while PayPal is not offered to the organisation (credentials not stored or the paypal_enabled flag off), and 503 paypal_unavailable when PayPal cannot be reached (nothing was charged; repeat the request).

  • Permission: billing.manage
  • API token: not available, use a signed-in session
ParameterInNotes
Idempotency-Keyheader

Request body (JSON):

{
  "amount_minor": 0,
  "currency": "USD"
}
ResponseMeaning
201Top-up created; send the customer to approve_url
defaultError

POST /api/v1/orgs/{org}/deposits/{deposit}/paypal/capture

Finish a PayPal top-up after approval

Called when the customer comes back from PayPal. The core reads the PayPal order recorded for this top-up (the request names no order), checks that it is the order made for this top-up, in US dollars and for exactly its amount, and that the top-up can still be paid, and only then captures it and adds the credit. Nothing is captured for an expired, cancelled or mismatching top-up. Repeating the call for a paid top-up returns it unchanged. Answers 409 paypal_not_approved while the customer has not approved, 409 topup_not_payable after expiry, 409 paypal_declined when PayPal declines, and 404 while PayPal is not offered or for a top-up of another organisation. A payment PayPal holds for review stays awaiting_payment until PayPal completes it.

  • Permission: billing.manage
  • API token: not available, use a signed-in session
ResponseMeaning
200The top-up as it stands (paid, or still awaiting_payment while PayPal reviews the payment)
defaultError

exports

GET /api/v1/orgs/{org}/exports

The organisation's data exports

The organisation's exports, newest first. Needs the manage permission (owner or admin). Panel session only.

  • Permission: org.manage
  • API token: not available, use a signed-in session
  • Session only: a person must be present
ResponseMeaning
200The exports
defaultError

POST /api/v1/orgs/{org}/exports

Ask for a copy of the organisation's data

Queues an export of one organisation's data (members, billing profile, servers, invoice references, support tickets, SSH keys, API token and webhook metadata, activity) as a zip of JSON files. Never another organisation's data, a secret, a signing secret or a variable. Needs the manage permission, a recent step-up, a panel session, and at most three a day for you.

  • Permission: org.manage
  • API token: not available, use a signed-in session
  • Step-up: needs a recent password check
  • Session only: a person must be present
ResponseMeaning
202Queued
defaultError

GET /api/v1/orgs/{org}/exports/{export}

One organisation data export

State, size, checksum and expiry of one of the organisation's exports. 404 for another organisation's.

  • Permission: org.manage
  • API token: not available, use a signed-in session
  • Session only: a person must be present
ResponseMeaning
200The export
defaultError

POST /api/v1/orgs/{org}/exports/{export}/download-link

Get a short-lived download link for an organisation export

As for a personal export. The link is used at /api/v1/me/export-downloads/{token}, and works only for someone who still has the manage permission when they use it.

  • Permission: org.manage
  • API token: not available, use a signed-in session
  • Step-up: needs a recent password check
  • Session only: a person must be present
ResponseMeaning
200The link
defaultError

features

GET /api/v1/orgs/{org}/features

Which features are on for the organisation

The effective value of every feature flag for this organisation, from the cached values (a staff change reaches here within 30 seconds). A flag the platform does not define is not listed and counts as off.

  • Permission: org.read
  • API token: not available, use a signed-in session
ResponseMeaning
200Features
defaultError

firewall-groups

GET /api/v1/orgs/{org}/firewall-groups

List firewall rule groups

Lists the organisation's rule groups with their rule counts and the number of policies using each, ordered by name.

  • Permission: servers.read
  • API token scope: firewall:read
ResponseMeaning
200Groups
defaultError

Example:

curl -X GET "$VEXELIA_URL/api/v1/orgs/$org/firewall-groups" \
  -H "Authorization: Bearer $VEXELIA_TOKEN"

POST /api/v1/orgs/{org}/firewall-groups

Create a firewall rule group

A group is an ordered rule list that is defined once and used by several firewall policies. At most 50 groups per organisation and 200 rules per group. Creating a group sends nothing to any server.

  • Permission: servers.manage
  • API token scope: firewall:write
ParameterInNotes
Idempotency-KeyheaderOptional with an API token: a new string (1 to 100 printable ASCII characters) per logical request. Repeating the request with the same key returns the first answer and does not repeat the action (response header Idempotent-Replay: true); the same key with a different request is 422 idempotency_key_reused. Kept 24 hours, per token and route.

Request body (JSON):

{
  "description": "string",
  "name": "string",
  "rules": [
    null
  ]
}
ResponseMeaning
201Created
defaultError

Example:

curl -X POST "$VEXELIA_URL/api/v1/orgs/$org/firewall-groups" \
  -H "Authorization: Bearer $VEXELIA_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"description":"string","name":"string","rules":[null]}'

DELETE /api/v1/orgs/{org}/firewall-groups/{group}

Delete a rule group

Refused with 409 firewall_group_in_use while any firewall policy uses the group.

  • Permission: servers.manage
  • API token scope: firewall:write
ParameterInNotes
Idempotency-KeyheaderOptional with an API token: a new string (1 to 100 printable ASCII characters) per logical request. Repeating the request with the same key returns the first answer and does not repeat the action (response header Idempotent-Replay: true); the same key with a different request is 422 idempotency_key_reused. Kept 24 hours, per token and route.
ResponseMeaning
204Done
defaultError

Example:

curl -X DELETE "$VEXELIA_URL/api/v1/orgs/$org/firewall-groups/$group" \
  -H "Authorization: Bearer $VEXELIA_TOKEN"

GET /api/v1/orgs/{org}/firewall-groups/{group}

Get a rule group with its rules, policies and servers

Returns the group's rules in evaluation order, the policies using it and every server that gets its rules with how far each has applied them. The version field is what If-Match takes on a later change.

  • Permission: servers.read
  • API token scope: firewall:read
ResponseMeaning
200Group
defaultError

Example:

curl -X GET "$VEXELIA_URL/api/v1/orgs/$org/firewall-groups/$group" \
  -H "Authorization: Bearer $VEXELIA_TOKEN"

PATCH /api/v1/orgs/{org}/firewall-groups/{group}

Change a rule group and send it to every server using it

Changing the rules bumps the version of every policy using the group and queues one apply per server using those policies; a busy server is applied as soon as it is free and shows pending. Refused with 409 firewall_too_many_rules when a policy using the group would pass 200 rules (its own plus its groups'); the message in fields.rules names the policy and a server, and nothing is changed. Refused with 409 firewall_lockout when it newly blocks the caller's own SSH on a server (acknowledge_lockout to apply anyway), and with 409 firewall_version_conflict when If-Match names an older version. A rename queues nothing.

  • Permission: servers.manage
  • API token scope: firewall:write
ParameterInNotes
If-MatchheaderThe firewall version you saw (the version field), as in "7". A different current version answers 409 firewall_version_conflict
acknowledge_lockoutquerytrue to apply a change that would block your own SSH after the 409 firewall_lockout warning
Idempotency-KeyheaderOptional with an API token: a new string (1 to 100 printable ASCII characters) per logical request. Repeating the request with the same key returns the first answer and does not repeat the action (response header Idempotent-Replay: true); the same key with a different request is 422 idempotency_key_reused. Kept 24 hours, per token and route.

Request body (JSON):

{
  "description": "string",
  "name": "string",
  "rules": [
    null
  ]
}
ResponseMeaning
200Changed
defaultError

Example:

curl -X PATCH "$VEXELIA_URL/api/v1/orgs/$org/firewall-groups/$group" \
  -H "Authorization: Bearer $VEXELIA_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"description":"string","name":"string","rules":[null]}'

firewall-presets

GET /api/v1/orgs/{org}/firewall-presets

List the firewall presets

The shipped starting sets: web server, SSH from one address, mail server, and block everything. A preset expands into ordinary rules that can then be edited; nothing remembers where a rule came from.

  • Permission: servers.read
  • API token scope: firewall:read
ResponseMeaning
200Presets
defaultError

Example:

curl -X GET "$VEXELIA_URL/api/v1/orgs/$org/firewall-presets" \
  -H "Authorization: Bearer $VEXELIA_TOKEN"

firewalls

GET /api/v1/orgs/{org}/firewalls

List firewall policies

Lists the organisation's firewall policies with their rule counts and the number of servers using each, ordered by name (case-insensitive) with the id as tiebreak. Without limit and cursor every policy is returned; with them the list is cursor paged (limit 1 to 200) and next_cursor is the cursor for the next page, null on the last. The rules of one policy are returned with it, at most 200 in evaluation order, and are not paged.

  • Permission: servers.read
  • API token scope: firewall:read
ParameterInNotes
limitqueryPage size, 1 to 200 (a larger value is lowered to 200). Without limit and cursor the whole list is returned, except where the operation says otherwise.
cursorqueryThe next_cursor of the previous page. Opaque; valid only for the list that issued it.
ResponseMeaning
200Policies
defaultError

Example:

curl -X GET "$VEXELIA_URL/api/v1/orgs/$org/firewalls" \
  -H "Authorization: Bearer $VEXELIA_TOKEN"

POST /api/v1/orgs/{org}/firewalls

Create a firewall policy

A new policy allows traffic in both directions until its rules and defaults say otherwise; locking down (inbound default drop) is an explicit choice. At most 50 policies per organisation.

  • Permission: servers.manage
  • API token scope: firewall:write
ParameterInNotes
Idempotency-KeyheaderOptional with an API token: a new string (1 to 100 printable ASCII characters) per logical request. Repeating the request with the same key returns the first answer and does not repeat the action (response header Idempotent-Replay: true); the same key with a different request is 422 idempotency_key_reused. Kept 24 hours, per token and route.

Request body (JSON):

{
  "default_inbound": "accept",
  "default_outbound": "accept",
  "description": "string",
  "name": "string"
}
ResponseMeaning
201Created
defaultError

Example:

curl -X POST "$VEXELIA_URL/api/v1/orgs/$org/firewalls" \
  -H "Authorization: Bearer $VEXELIA_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"default_inbound":"accept","default_outbound":"accept","description":"string","name":"string"}'

DELETE /api/v1/orgs/{org}/firewalls/{firewall}

Delete a firewall policy

Refused with firewall_in_use while a provisioning, active or suspended server uses the policy. Terminated or failed servers that still name it are detached as it is deleted.

  • Permission: servers.manage
  • API token scope: firewall:write
ParameterInNotes
Idempotency-KeyheaderOptional with an API token: a new string (1 to 100 printable ASCII characters) per logical request. Repeating the request with the same key returns the first answer and does not repeat the action (response header Idempotent-Replay: true); the same key with a different request is 422 idempotency_key_reused. Kept 24 hours, per token and route.
ResponseMeaning
204Done
defaultError

Example:

curl -X DELETE "$VEXELIA_URL/api/v1/orgs/$org/firewalls/$firewall" \
  -H "Authorization: Bearer $VEXELIA_TOKEN"

GET /api/v1/orgs/{org}/firewalls/{firewall}

Get a firewall policy with its rules and servers

Returns one firewall with its rules in evaluation order, the servers using it, and a lockout warning when the policy would block SSH. The version field is what If-Match takes on a later change.

  • Permission: servers.read
  • API token scope: firewall:read
ResponseMeaning
200Policy
defaultError

Example:

curl -X GET "$VEXELIA_URL/api/v1/orgs/$org/firewalls/$firewall" \
  -H "Authorization: Bearer $VEXELIA_TOKEN"

PATCH /api/v1/orgs/{org}/firewalls/{firewall}

Rename a firewall policy or change its defaults

Changing a default queues an apply on every server using the policy. A rename queues nothing. A change that newly blocks the caller's own SSH on a server using the policy is refused with 409 firewall_lockout (the sentence is in fields.lockout) unless acknowledge_lockout is true. With If-Match naming the version you saw, a changed policy answers 409 firewall_version_conflict.

  • Permission: servers.manage
  • API token scope: firewall:write
ParameterInNotes
If-MatchheaderThe firewall version you saw (the version field), as in "7". A different current version answers 409 firewall_version_conflict
acknowledge_lockoutquerytrue to apply a change that would block your own SSH after the 409 firewall_lockout warning
Idempotency-KeyheaderOptional with an API token: a new string (1 to 100 printable ASCII characters) per logical request. Repeating the request with the same key returns the first answer and does not repeat the action (response header Idempotent-Replay: true); the same key with a different request is 422 idempotency_key_reused. Kept 24 hours, per token and route.

Request body (JSON):

{
  "default_inbound": "accept",
  "default_outbound": "accept",
  "description": "string",
  "name": "string"
}
ResponseMeaning
200Changed
defaultError

Example:

curl -X PATCH "$VEXELIA_URL/api/v1/orgs/$org/firewalls/$firewall" \
  -H "Authorization: Bearer $VEXELIA_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"default_inbound":"accept","default_outbound":"accept","description":"string","name":"string"}'

PUT /api/v1/orgs/{org}/firewalls/{firewall}/groups

Set the rule groups a firewall policy uses

Replaces the policy's groups with the list given, each at a position (the number of the policy's own rules, per direction, that come before the group's rules there). The group's rules count toward the policy's 200-rule limit; a list that would pass it is refused with 409 firewall_too_many_rules, naming a server. Queues an apply on every server using the policy. Refused with 409 firewall_lockout (acknowledge_lockout to apply anyway) and 409 firewall_version_conflict as for rule changes. A group from another organisation answers 404.

  • Permission: servers.manage
  • API token scope: firewall:write
ParameterInNotes
If-MatchheaderThe firewall version you saw (the version field), as in "7". A different current version answers 409 firewall_version_conflict
acknowledge_lockoutquerytrue to apply a change that would block your own SSH after the 409 firewall_lockout warning
Idempotency-KeyheaderOptional with an API token: a new string (1 to 100 printable ASCII characters) per logical request. Repeating the request with the same key returns the first answer and does not repeat the action (response header Idempotent-Replay: true); the same key with a different request is 422 idempotency_key_reused. Kept 24 hours, per token and route.

Request body (JSON):

{
  "groups": [
    null
  ]
}
ResponseMeaning
200Changed
defaultError

Example:

curl -X PUT "$VEXELIA_URL/api/v1/orgs/$org/firewalls/$firewall/groups" \
  -H "Authorization: Bearer $VEXELIA_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"groups":[null]}'

GET /api/v1/orgs/{org}/firewalls/{firewall}/lockout-check

Whether the policy would block your own SSH connection

Follows the policy's inbound rules for a TCP connection to port 22 from the address the API sees you at, and says what decides it. Advisory: the address is yours only if you connect to the server from where you use the panel. The source and the verdict are null when the API has no address that could be yours (a private or loopback address). The same check refuses changes that newly block you.

  • Permission: servers.read
  • API token scope: firewall:read
ResponseMeaning
200Verdict
defaultError

Example:

curl -X GET "$VEXELIA_URL/api/v1/orgs/$org/firewalls/$firewall/lockout-check" \
  -H "Authorization: Bearer $VEXELIA_TOKEN"

POST /api/v1/orgs/{org}/firewalls/{firewall}/presets/{preset}

Apply a preset to a firewall policy

Writes the preset into the policy as ordinary rules (replace or append, said in the request) and queues an apply on every server using the policy. Refused with 409 firewall_limit when the rules would not fit, firewall_lockout when it would newly block your own SSH on a server using the policy (acknowledge_lockout to apply anyway), and firewall_version_conflict when If-Match names an older version.

  • Permission: servers.manage
  • API token scope: firewall:write
ParameterInNotes
If-MatchheaderThe firewall version you saw (the version field), as in "7". A different current version answers 409 firewall_version_conflict
acknowledge_lockoutquerytrue to apply a change that would block your own SSH after the 409 firewall_lockout warning
Idempotency-KeyheaderOptional with an API token: a new string (1 to 100 printable ASCII characters) per logical request. Repeating the request with the same key returns the first answer and does not repeat the action (response header Idempotent-Replay: true); the same key with a different request is 422 idempotency_key_reused. Kept 24 hours, per token and route.

Request body (JSON):

{
  "address": "string",
  "mode": "replace"
}
ResponseMeaning
200Applied
defaultError

Example:

curl -X POST "$VEXELIA_URL/api/v1/orgs/$org/firewalls/$firewall/presets/$preset" \
  -H "Authorization: Bearer $VEXELIA_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"address":"string","mode":"replace"}'

POST /api/v1/orgs/{org}/firewalls/{firewall}/presets/{preset}/preview

What applying a preset would change

Writes nothing. replace removes every rule the policy has and sets the preset's defaults; append adds the preset's rules after the existing ones and leaves the defaults. A preset that asks for an address answers 422 on address when it is missing or not an address.

  • Permission: servers.read
  • API token scope: firewall:read

Request body (JSON):

{
  "address": "string",
  "mode": "replace"
}
ResponseMeaning
200The difference
defaultError

Example:

curl -X POST "$VEXELIA_URL/api/v1/orgs/$org/firewalls/$firewall/presets/$preset/preview" \
  -H "Authorization: Bearer $VEXELIA_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"address":"string","mode":"replace"}'

POST /api/v1/orgs/{org}/firewalls/{firewall}/rules

Add a rule at the end of a direction

Validates the rule, then queues an apply on every server using the policy. Addresses may be CIDR blocks, single IPv4 or IPv6 addresses, or ranges; ranges are stored as the fewest CIDR blocks that cover them. At most 200 rules per policy. Rules cannot match link-local or multicast addresses, which Vexelia manages for the server. Refused with 409 firewall_lockout when it newly blocks the caller's own SSH (see PATCH on the policy), and with 409 firewall_version_conflict when If-Match names an older version.

  • Permission: servers.manage
  • API token scope: firewall:write
ParameterInNotes
If-MatchheaderThe firewall version you saw (the version field), as in "7". A different current version answers 409 firewall_version_conflict
acknowledge_lockoutquerytrue to apply a change that would block your own SSH after the 409 firewall_lockout warning
Idempotency-KeyheaderOptional with an API token: a new string (1 to 100 printable ASCII characters) per logical request. Repeating the request with the same key returns the first answer and does not repeat the action (response header Idempotent-Replay: true); the same key with a different request is 422 idempotency_key_reused. Kept 24 hours, per token and route.

Request body (JSON):

{}
ResponseMeaning
201Added
defaultError

Example:

curl -X POST "$VEXELIA_URL/api/v1/orgs/$org/firewalls/$firewall/rules" \
  -H "Authorization: Bearer $VEXELIA_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{}'

POST /api/v1/orgs/{org}/firewalls/{firewall}/rules/reorder

Set the order of the rules in one direction

The list must name every rule of that direction exactly once, highest priority first; otherwise 422 on rule_ids, so a stale editor cannot drop a rule added elsewhere.

  • Permission: servers.manage
  • API token scope: firewall:write
ParameterInNotes
If-MatchheaderThe firewall version you saw (the version field), as in "7". A different current version answers 409 firewall_version_conflict
acknowledge_lockoutquerytrue to apply a change that would block your own SSH after the 409 firewall_lockout warning
Idempotency-KeyheaderOptional with an API token: a new string (1 to 100 printable ASCII characters) per logical request. Repeating the request with the same key returns the first answer and does not repeat the action (response header Idempotent-Replay: true); the same key with a different request is 422 idempotency_key_reused. Kept 24 hours, per token and route.

Request body (JSON):

{
  "direction": "inbound",
  "rule_ids": [
    null
  ]
}
ResponseMeaning
200Reordered
defaultError

Example:

curl -X POST "$VEXELIA_URL/api/v1/orgs/$org/firewalls/$firewall/rules/reorder" \
  -H "Authorization: Bearer $VEXELIA_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"direction":"inbound","rule_ids":[null]}'

DELETE /api/v1/orgs/{org}/firewalls/{firewall}/rules/{rule}

Remove a rule

Closes the gap in the order and queues an apply on every server using the policy. Answers with the policy.

  • Permission: servers.manage
  • API token scope: firewall:write
ParameterInNotes
If-MatchheaderThe firewall version you saw (the version field), as in "7". A different current version answers 409 firewall_version_conflict
acknowledge_lockoutquerytrue to apply a change that would block your own SSH after the 409 firewall_lockout warning
Idempotency-KeyheaderOptional with an API token: a new string (1 to 100 printable ASCII characters) per logical request. Repeating the request with the same key returns the first answer and does not repeat the action (response header Idempotent-Replay: true); the same key with a different request is 422 idempotency_key_reused. Kept 24 hours, per token and route.
ResponseMeaning
200Removed
defaultError

Example:

curl -X DELETE "$VEXELIA_URL/api/v1/orgs/$org/firewalls/$firewall/rules/$rule" \
  -H "Authorization: Bearer $VEXELIA_TOKEN"

PATCH /api/v1/orgs/{org}/firewalls/{firewall}/rules/{rule}

Change a rule

Changes one rule. Send only the fields to change. A change that would cut your own SSH answers 409 firewall_lockout unless acknowledge_lockout=true.

  • Permission: servers.manage
  • API token scope: firewall:write
ParameterInNotes
If-MatchheaderThe firewall version you saw (the version field), as in "7". A different current version answers 409 firewall_version_conflict
acknowledge_lockoutquerytrue to apply a change that would block your own SSH after the 409 firewall_lockout warning
Idempotency-KeyheaderOptional with an API token: a new string (1 to 100 printable ASCII characters) per logical request. Repeating the request with the same key returns the first answer and does not repeat the action (response header Idempotent-Replay: true); the same key with a different request is 422 idempotency_key_reused. Kept 24 hours, per token and route.

Request body (JSON):

{}
ResponseMeaning
200Changed
defaultError

Example:

curl -X PATCH "$VEXELIA_URL/api/v1/orgs/$org/firewalls/$firewall/rules/$rule" \
  -H "Authorization: Bearer $VEXELIA_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{}'

floating-ip-charges

POST /api/v1/orgs/{org}/floating-ip-charges/{charge}/pay

Pay for buying a floating IP

Pays a floating IP charge: wallet credit is held first, then one card intent covers the rest (the client secret is returned for Stripe.js), or the charge settles at once when credit covers it. Settlement issues an invoice, takes an address from the location's pool and makes the floating IP (billed from then), and voids the renewal that was issued without it. If the pool ran out between the quote and the payment the charge is credited back whole to the wallet. Repeating the call shows the same intent. quote_expired after 30 minutes (ask again), paid_period_ended if a renewal settled or the period ended since the quote, amount_below_minimum when the card part is below what the processor can charge.

  • Permission: servers.manage
  • API token: not available, use a signed-in session
  • Step-up: needs a recent password check

Request body (JSON):

{
  "payment_method": "00000000-0000-0000-0000-000000000000"
}
ResponseMeaning
200Where the payment stands
defaultError

floating-ips

GET /api/v1/orgs/{org}/floating-ips

Every floating IP of the organisation, with price and limits

The organisation's floating IPs (the Floating IPs page), newest first, with the server that pays for each, the one it is assigned to and, during a reassign, the one waiting for it; the monthly price of one, the monthly total, the floating IP limit, open_charges (floating IP charges waiting to be paid: quoted and not yet expired; pay one with POST .../floating-ip-charges/{charge}/pay until expires_at) and servers, the active servers a floating IP can be bought for or assigned to, each with an opaque location label. A floating IP is a routable IPv4 address that belongs to the organisation and can be assigned to any of its servers in the same location (equal location labels), one at a time; it is not part of any server and outlives the server it is on. Released floating IPs are not listed.

  • Permission: servers.read
  • API token scope: servers:read
ResponseMeaning
200Floating IPs, limits and price
defaultError

Example:

curl -X GET "$VEXELIA_URL/api/v1/orgs/$org/floating-ips" \
  -H "Authorization: Bearer $VEXELIA_TOKEN"

POST /api/v1/orgs/{org}/floating-ips

Buy a floating IP

Prices a new floating IP named name, bought for the server server_id, whose subscription pays for it and whose location the address comes from. The charge is the monthly price for the rest of the period the customer has paid for (remaining over total as an exact duration, rounded half up once), plus tax. The response carries it as charge, which stands for 30 minutes and is paid with POST .../floating-ip-charges/{charge}/pay (credit first, then card or 3-D Secure); the floating IP and its address exist and are billed only after payment settles. A charge below the minimum a card can be charged is added to the next renewal invoice instead (deferred is true and the floating IP exists at once), and a charge of zero makes it at once (charge is null). The renewal after the paid period bills it whole, on that server's subscription. The new floating IP is not assigned to any server; assign it with POST .../assign. Asking again for the same name while the quote stands returns the same quote. Refused with floating_ip_name_taken when one has the name, floating_ip_limit at the limit, floating_ip_no_pool when the location has no floating address to give, floating_ip_host_unsupported when no active host in the location reports that it applies floating IPs, renewal_charging while the server's renewal is being paid, volume_charge_open while another volume or floating IP charge is waiting to be paid, server_not_active when the server is not an active VPS, paid_period_ended when the paid period has ended. Answers 503 tax_unavailable while tax cannot be worked out yet for the buyer's location; nothing is created or charged.

  • Permission: servers.manage
  • API token: not available, use a signed-in session
  • Step-up: needs a recent password check

Request body (JSON):

{
  "name": "string",
  "server_id": "00000000-0000-0000-0000-000000000000"
}
ResponseMeaning
202A charge to pay first, or the floating IP when there was nothing to pay
defaultError

DELETE /api/v1/orgs/{org}/floating-ips/{floating_ip}

Release a floating IP

Gives a floating IP that is not assigned back to the pool: the address goes into quarantine for 7 days like a server's, and any reverse DNS record for it is removed. It happens at once (no host is involved). There is no credit for the rest of the paid period (decision 0008 applied), so the period stands as paid and the next renewal no longer bills it. A floating IP a server holds is never released this way: it is refused with floating_ip_assigned (unassign it first), so a delete can not take an address off a live server. Refused with floating_ip_busy while it is changing.

  • Permission: servers.manage
  • API token: not available, use a signed-in session
  • Step-up: needs a recent password check
ResponseMeaning
204Released
defaultError

GET /api/v1/orgs/{org}/floating-ips/{floating_ip}

One floating IP

One floating IP of the organisation, as the list shows it. A released floating IP is a 404.

  • Permission: servers.read
  • API token scope: servers:read
ResponseMeaning
200The floating IP
defaultError

Example:

curl -X GET "$VEXELIA_URL/api/v1/orgs/$org/floating-ips/$floating_ip" \
  -H "Authorization: Bearer $VEXELIA_TOKEN"

POST /api/v1/orgs/{org}/floating-ips/{floating_ip}/assign

Assign a floating IP to a server

Assigns an unassigned floating IP to the server server_id. The server's host routes the address to the server and lets it send from it; the guest is not configured, so the address must be added inside the guest (as an extra address on its network interface). The server must be an active VPS in the floating IP's location whose host reports that it applies floating IPs. The assign is one operation: the floating IP reads assigning until the host answers, assigned after, and unassigned again with error_code if it could not. Reverse DNS set for the address follows it to the server. Refused with floating_ip_assigned when it is assigned already, floating_ip_busy while it is changing, floating_ip_other_group when the server is in another location, floating_ip_server_full when the server holds as many as a server may, floating_ip_billing_ended when the server that pays for the floating IP has ended (release it, or contact support to move its billing), floating_ip_other_node when the server is on another host from the one the address was first assigned on (unless staff allowed the pool to move between hosts), server_not_active when the server is not an active VPS, renewal_charging while its renewal is being paid, floating_ip_host_unsupported when the host has not reported that it applies floating IPs (an older host software version), server_busy while another action runs on the server.

  • Permission: servers.manage
  • API token: not available, use a signed-in session
  • Step-up: needs a recent password check

Request body (JSON):

{
  "server_id": "00000000-0000-0000-0000-000000000000"
}
ResponseMeaning
202Queued
defaultError

POST /api/v1/orgs/{org}/floating-ips/{floating_ip}/reassign

Move a floating IP to another server

Moves the floating IP from the server that holds it to server_id, another active server in the same location. It is two host operations in order, so two servers never answer for the address: the address is taken off the first server (one operation, queued now) and, when the host has done that, put on the second (queued by the platform shortly after). The address is unreachable between the two. Because it stops working on the first server at once, the body must say confirm true (floating_ip_confirm_required otherwise). While waiting, the floating IP is unassigned with target_server_id set; if the new server ends or can not take it within 24 hours the move is dropped, the floating IP stays unassigned and error_code says why. Reverse DNS follows the address to the new server. The new server is checked with exactly the rules an assign applies, before anything is done to the first one, so a move that could not finish never takes the address off. Refused as assign and unassign are, and with a 422 on server_id when it is the server that holds it now.

  • Permission: servers.manage
  • API token: not available, use a signed-in session
  • Step-up: needs a recent password check

Request body (JSON):

{
  "confirm": false,
  "server_id": "00000000-0000-0000-0000-000000000000"
}
ResponseMeaning
202The unassign from the first server is queued
defaultError

POST /api/v1/orgs/{org}/floating-ips/{floating_ip}/unassign

Take a floating IP off its server

Takes the floating IP off the server that holds it. Traffic to the address stops reaching that server as soon as the host has applied it, so the body must say confirm true: without it nothing happens and the answer is floating_ip_confirm_required. The floating IP stays the organisation's and stays billed; it can be assigned again or released. Refused with floating_ip_not_assigned when no server holds it, floating_ip_busy while it is changing, floating_ip_host_unsupported when the host has not reported that it applies floating IPs, server_busy while another action runs on the server.

  • Permission: servers.manage
  • API token: not available, use a signed-in session
  • Step-up: needs a recent password check

Request body (JSON):

{
  "confirm": false
}
ResponseMeaning
202Queued
defaultError

game-orders

POST /api/v1/orgs/{org}/game-orders

Order a game server

Orders a game server on a game plan: the plan's price, a region, a name and the game (template_id) the plan runs, with optional automatic renewal. Needs a verified email (email_unverified). Priced, taxed and paid like a server order (credit is held, then the card is charged); answers 201 with the order and a client_secret when a card payment is needed. Startup variables that break the game's rules refuse the order (422, field variables.KEY) before anything is held. Refused with no_capacity when no game node in the region has room, and with game_unavailable when that game cannot be built right now (no published version or no signed image), in which case nothing is charged. When payment arrives the server is built; if it cannot be built as bought the payment is refunded. A token is never allowed to order.

  • Permission: games.manage
  • API token: not available, use a signed-in session
ParameterInNotes
Idempotency-Keyheader

Request body (JSON):

{
  "autopay": false,
  "name": "string",
  "price_id": "00000000-0000-0000-0000-000000000000",
  "region": "string",
  "template_id": "string",
  "variables": {}
}
ResponseMeaning
201Order priced; pay with client_secret
defaultError

GET /api/v1/orgs/{org}/game-orders/{order}

Get a game server order

Returns a game server order and, once paid, the server id and how its setup stands (setup: provisioning, active or failed). A VPS order, or an order of another organisation, is not found. Poll it after placing an order.

  • Permission: games.read
  • API token: not available, use a signed-in session
ResponseMeaning
200Order
defaultError

game-plans

GET /api/v1/game-plans

Game plans on sale, with slots, resources and prices

Only plans that are active and have a price in force. Hidden and retired plans never appear. With the template registry present, a plan whose every template was removed is left out, and the template query parameter keeps only plans big enough for that template. Buying a game server is a later milestone.

  • API token: not available, use a signed-in session
ParameterInNotes
templatequeryKeep only plans that can run this template
ResponseMeaning
200Game plans
defaultError

game-templates

GET /api/v1/game-templates

Games on sale, with the startup variables a customer may set

The newest published version of every game that a plan on sale runs, with the startup variables the customer may set when ordering (editable ones that are not secrets), each with its type, default and limits, so the order page can offer the right control. The server checks every value again when the order is placed. A secret is set once the server exists (has_secrets says the game has one). Nothing of the installer, the image or the start command is shown.

  • API token: not available, use a signed-in session
ResponseMeaning
200Games
defaultError

games

GET /api/v1/orgs/{org}/games

List game servers

The organisation's game servers, newest first, with the state the node last reported. Creating one is part of the purchase flow.

  • Permission: games.read
  • API token: not available, use a signed-in session
ResponseMeaning
200Game servers
defaultError

GET /api/v1/orgs/{org}/games/{game}

Show a game server

The server with its resources, ports and startup variables. Secret variables show only whether one is set. A server of another organisation, or a VPS, is not found.

  • Permission: games.read
  • API token: not available, use a signed-in session
ResponseMeaning
200Game server
defaultError

PATCH /api/v1/orgs/{org}/games/{game}

Rename a game server or change its variables

Fields left out keep their value. Variables are checked against the template's types and limits; a variable that is read-only, unknown, or no longer declared by the template is refused, and one refusal rejects the whole change. A change is recorded in Activity by key; for a secret only that it changed.

  • Permission: games.manage
  • API token: not available, use a signed-in session

Request body (JSON):

{
  "name": "string",
  "variables": {}
}
ResponseMeaning
200Updated server
defaultError

GET /api/v1/orgs/{org}/games/{game}/access

List who has been given access to a game server

The server's invitations and guests (live ones first, then the last ones that ended), the five rights a grant can carry, which of them the caller may give (`grantable`: a right is given only by someone who holds it), and the limits. A guest is another account with a scoped part of one server and no membership of the organisation. A server of another organisation is not found. Needs games.subusers (owners and admins by default); no API token scope opens it. State: integrated on fakes.

  • Permission: games.subusers
  • API token: not available, use a signed-in session
ResponseMeaning
200The access list
defaultError

POST /api/v1/orgs/{org}/games/{game}/access

Invite another account to a game server

Invites the person at `email` with one or more rights: console, power, files, backups, schedules. They are sent an email (no link that works by itself: they sign in with that address, or create an account with it, and accept on the Games page), and the invitation can be accepted for 14 days. Seeing the server, its state, address and graphs is implied. A right is given only by someone who holds it (grant_escalation otherwise), and the check is made again when the invitation is accepted (invite_inviter_changed if the person who set it can no longer give it). Needs step-up. Refused with subuser_exists for a person with a live invitation or access, and subuser_limit_reached at the server's limit (an invitation past its time is closed first and does not count). The email goes to an address that may not have asked for it, so the MAIL is limited: one an hour to the same address for the same server, three a day to one address across all organisations, ten a day from one person and twenty a day from one organisation (counted in keyed fingerprints, never the address; plus-tags and gmail dots do not make a new address). A limit never blocks the invitation: it is still made and listed (and the person sees it under their own invitations), and the answer says `mail_sent: false`. The names in the mail are quoted and marked as the sender's. Audited as game.subuser_invited, with no address in the event. Needs games.subusers. State: integrated on fakes.

  • Permission: games.subusers
  • API token: not available, use a signed-in session
  • Step-up: needs a recent password check

Request body (JSON):

{
  "email": "string",
  "permissions": [
    null
  ]
}
ResponseMeaning
201Invited
defaultError

DELETE /api/v1/orgs/{org}/games/{game}/access/{grant}

Revoke an invitation or a grant on a game server

Ends a pending invitation or an active grant. It takes effect on the guest's next request, and any console they have open on the server ends at the gateway's next heartbeat. Needs no step-up: taking access away is never held up. A short-lived SFTP credential the guest already holds stays valid until it expires (10 minutes; the SFTP service itself is not built). Audited as game.subuser_revoked. Needs games.subusers. State: integrated on fakes.

  • Permission: games.subusers
  • API token: not available, use a signed-in session
ResponseMeaning
204Revoked
defaultError

PUT /api/v1/orgs/{org}/games/{game}/access/{grant}

Change what an invited account may do on a game server

Replaces the rights of a pending or active grant. The new set must be one the caller holds in full (grant_escalation). It is read on every request, so a right taken away is gone on the guest's next one, and a console that was open when the console right is taken away ends at the gateway's next heartbeat (within seconds). Needs step-up. Audited as game.subuser_changed. Needs games.subusers. State: integrated on fakes.

  • Permission: games.subusers
  • API token: not available, use a signed-in session
  • Step-up: needs a recent password check

Request body (JSON):

{
  "permissions": [
    null
  ]
}
ResponseMeaning
200Changed
defaultError

GET /api/v1/orgs/{org}/games/{game}/backups

List a game server's backups

The server's backups, newest first (at most 200; a deleted one is not listed), the restores asked for on it (the latest 20), what the plan allows (`limits.backups` backups at once, `limits.used` held now, `limits.max_locked` of them lockable, one slot always stays free so a scheduled backup can replace the oldest), and why a new backup or a restore would be refused right now (`can_create`, `can_restore`) so the panel can say so first. A backup is a copy of the server's data directory only: never its environment, its startup command, its secret variables or the host's files. It IS the whole data directory, so a config file the game keeps a secret in is in it, encrypted under your organisation's key; a game's template can name such files to leave out. State pending means waiting for the host; running, the host is archiving it; ready, its manifest was read back and checked; failed, with `error_code` (store_unavailable, backup_too_large, backup_too_many_files, node_no_backups, window_missed, server_busy, superseded and others); deleting, its stored objects are being removed. A ready backup may carry `error_code` resume_failed: the game did not confirm that writes were switched back on after the backup, so restart the server. `quiesced` says whether the game confirmed it had stopped writing first; false means the copy is crash-consistent (like a power cut), which games recover from. A server of another organisation is not found. Needs games.backups; no API token scope opens it. State: integrated on fakes; no real container node, object store or key service has run this.

  • Permission: games.backups
  • API token: not available, use a signed-in session
ResponseMeaning
200The backups
defaultError

POST /api/v1/orgs/{org}/games/{game}/backups

Back a game server up now

Records a backup as pending and answers 202; the worker queues the host's job as soon as the server's lane is free. Only one backup of a server waits or runs at a time (backup_in_progress), and while it runs a graceful stop or restart answers server_busy (a kill still works and ends the backup as superseded). Works while the server runs (the template's save commands are sent first and resume afterwards) and while it is stopped. A manual backup at the plan's count is refused with backup_limit_reached: delete one first (a scheduled backup at the count goes ahead if an unlocked scheduled one can be replaced, and only once it is verified ready does the oldest go: a failed backup never costs a restore point); backups of a plan that has none are refused with backups_not_included. Refused with backup_daily_cap (429) after twice the plan's count of backups in 24 hours, whatever became of them, and with server_not_active unless the server is active, and backups_host_unsupported when the host's software has not reported that it makes backups (nothing is queued). The size is capped by the plan's disk and the platform's own ceiling; a larger one fails as backup_too_large. A backup is encrypted with a key that belongs to your organisation before it leaves the host. A server of another organisation is not found. Needs games.backups.

  • Permission: games.backups
  • API token: not available, use a signed-in session

Request body (JSON):

{
  "name": "string"
}
ResponseMeaning
202Recorded
defaultError

DELETE /api/v1/orgs/{org}/games/{game}/backups/{backup}

Delete a game server backup

Marks a ready or failed backup for deletion and answers 202; the worker then removes every stored object with the credential only it holds, and the backup disappears from the list. A locked backup is refused with backup_locked; one that is still waiting or running with backup_state (as is one a restore is waiting on or running from). A backup of another server or organisation is not found. Needs games.backups.

  • Permission: games.backups
  • API token: not available, use a signed-in session
ResponseMeaning
202Deleting
defaultError

PATCH /api/v1/orgs/{org}/games/{game}/backups/{backup}

Lock or unlock a game server backup

A locked backup is never removed by retention and cannot be deleted until it is unlocked. At most one fewer than the plan's count may be locked (backup_lock_limit), so a scheduled backup can always replace the oldest unlocked one. A failed or deleting backup cannot be locked (backup_state). A backup of another server or organisation is not found. Needs games.backups.

  • Permission: games.backups
  • API token: not available, use a signed-in session

Request body (JSON):

{
  "locked": false
}
ResponseMeaning
200The backup
defaultError

POST /api/v1/orgs/{org}/games/{game}/backups/{backup}/restore

Restore a backup over a stopped game server

Replaces the server's files with the backup's and answers 202; the worker queues the host's job. The server must be stopped (offline or crashed) when this is asked and again when the job is queued, or the restore fails as server_not_stopped with nothing changed (restore_needs_stopped answers now). Needs step-up and the server's name in `confirm_name` (case and spaces do not matter; validation_failed otherwise). The host first reads and checks every part of the backup and unpacks it beside the live files; a damaged backup is refused (corrupt) with the server's files untouched. The files it replaces are kept beside them on the host as the pre-restore copy until the next restore. The server stays in state restoring until the host reports; a restore left in review is shown to support. Only one restore of a server is open at a time (restore_in_progress). Refused with backup_state unless the backup is ready. A backup of another server or organisation is not found. Needs games.backups. State: integrated on fakes; the pre-restore copy's 72-hour removal, the quota and ownership of the staging directory and a real object store are not built or run.

  • Permission: games.backups
  • API token: not available, use a signed-in session
  • Step-up: needs a recent password check

Request body (JSON):

{
  "confirm_name": "string"
}
ResponseMeaning
202Recorded
defaultError

GET /api/v1/orgs/{org}/games/{game}/config

List the config files of a game server that the editor can open

The files the server's signed template declares as editable (`config_editors`), with the format each is checked against and whether a save needs a restart. `managed_keys` are keys the platform itself sets at every start (the template's `config_files`), which an edit to them does not survive. Needs games.files and a server whose node is reachable (the refusals of the file list apply). Built on the file manager's typed requests (GF-1), and integrated on fakes only: it has not run against a real container node.

  • Permission: games.files
  • API token: not available, use a signed-in session
ResponseMeaning
200The editors
defaultError

GET /api/v1/orgs/{org}/games/{game}/config/file

Open a config file of a game server with its saved versions

Returns the text, the `version` to send back when saving, and the saved versions (newest first, without their text). A file that does not exist yet comes back with `exists` false and empty text. A path the template does not declare is config_path_unknown (404). Opening is recorded in Activity by path and size, never content.

  • Permission: games.files
  • API token: not available, use a signed-in session
ParameterInNotes
pathquery
ResponseMeaning
200The file
defaultError

PUT /api/v1/orgs/{org}/games/{game}/config/file

Save a config file of a game server after checking its syntax

Checks the text against the file's format first (json, yaml, properties and ini; `text` is not checked) and refuses a syntax error with validation_failed, naming the line in `fields.content`; nothing is written. Writes over the `version` that was opened (file_conflict, 409, if the file changed since; a file that does not exist is created and takes no version) through the file manager's atomic write, so the denylist, quota and ownership rules apply. What the file held before is kept first when no saved version is that text (as `baseline`, or `external` if it was changed elsewhere), then the new text is kept as a version; the newest 20 per file are kept. The text of a version is sealed, because a config file can hold a password. `restart_required` is the hint to restart the server for the change to apply. Saving the text the file already holds changes nothing (`changed` false). Recorded in Activity by path, size and the number of lines changed, never the text.

  • Permission: games.files
  • API token: not available, use a signed-in session

Request body (JSON):

{
  "content": "string",
  "path": "string",
  "version": "string"
}
ResponseMeaning
200Saved
defaultError

POST /api/v1/orgs/{org}/games/{game}/config/undo

Restore an earlier saved version of a config file

Writes a saved version back as the file, the same way a save does: over the `version` the editor last saw (file_conflict if it changed), keeping what is there first so the undo can itself be undone, and recording the restore as a new version of source `undo`. Recorded in Activity.

  • Permission: games.files
  • API token: not available, use a signed-in session

Request body (JSON):

{
  "path": "string",
  "version": "string",
  "version_id": "00000000-0000-0000-0000-000000000000"
}
ResponseMeaning
200Restored
defaultError

GET /api/v1/orgs/{org}/games/{game}/config/version

Read one saved version of a config file

The text of one saved version, to look at before restoring it. Another file's version, or another server's, is not found.

  • Permission: games.files
  • API token: not available, use a signed-in session
ParameterInNotes
pathquery
idquery
ResponseMeaning
200The version
defaultError

POST /api/v1/orgs/{org}/games/{game}/console

Get a single-use token to open a game server's console

Returns a token valid for 60 seconds and one connection, and the edgegw WebSocket URL for the game server's text console: the container's output, and its standard input for the commands the person types. The browser opens `new WebSocket(ws_url, [protocol, token])`; the token travels in Sec-WebSocket-Protocol and never in a URL. edgegw verifies the token, refuses a second use, and checks the person's games.manage permission, the server and the caps again when the token is used and while the session lasts; the node verifies the token again. A new session starts with the recent output the node saw (not a durable log). What is typed or shown is never recorded. Refused with console_server_stopped (the server is not running), console_server_suspended, server_not_active (still being set up), server_busy (being created or removed), console_node_unreachable or console_session_limit (one session per server, four per person), and console_unavailable while the console is turned off. A server of another organisation, or a VPS, is not found. State: GC-1 is integrated on fakes only. On a real container runtime the console does NOT work yet: Containerd.Attach is read-only and reaches EOF immediately (it reads the existing log once, does not tail it, and refuses input), so it works only after GH-R4 (containerd console attach: tail the log + stdin).

  • Permission: games.manage
  • API token: not available, use a signed-in session
  • Session only: a person must be present

Request body (JSON):

{}
ResponseMeaning
201Token minted; it is not stored and is shown only here
defaultError

POST /api/v1/orgs/{org}/games/{game}/destroy

Destroy a game server

Ends the server and its billing and asks the node to remove it; when the node confirms, its address range and ports are given back. Needs the server's name in confirm_name and a recent step-up. Refused with server_not_active while the server is still being set up, and with server_protected while it is marked do-not-destroy. There is no trash: a destroyed game server's data is gone.

  • Permission: servers.destroy
  • API token: not available, use a signed-in session
  • Step-up: needs a recent password check

Request body (JSON):

{
  "confirm_name": "string"
}
ResponseMeaning
202Terminating
defaultError

GET /api/v1/orgs/{org}/games/{game}/eula

Show a game server's EULA and whether it is accepted

Returns the current EULA text for the server's game and whether this server has accepted that version, and by whom and when. Returns eula_unavailable when no text is published for the game. A server of another organisation is not found.

  • Permission: games.read
  • API token: not available, use a signed-in session
ResponseMeaning
200The current EULA
defaultError

POST /api/v1/orgs/{org}/games/{game}/eula

Accept a game server's current EULA

Records that the signed-in customer accepted the given EULA version for this server (who, organisation, time, version, text hash and address). The version must be the current one, otherwise eula_version_changed: read the new text and accept it. A new version needs a new acceptance before the next start. Accepting again is a no-op. A panel session only: an API token or a staff viewing session cannot accept.

  • Permission: games.manage
  • API token: not available, use a signed-in session
  • Session only: a person must be present

Request body (JSON):

{
  "version": 0
}
ResponseMeaning
200Accepted
defaultError

GET /api/v1/orgs/{org}/games/{game}/files

List a folder of a game server

Lists the folder at `path` (default: the server's data directory). The node lists at most 5000 items: the first 5000 by name, then shown folders first within that page, so the page is the same each time; `truncated` says there were more. Every path is relative to the server's own data directory: `..`, `.`, backslashes and control characters are refused with file_path_invalid, and a leading slash is read as the data directory, never as the host. A symbolic link is listed as kind `link` and a device, pipe or socket as `special`; neither is ever followed or opened. Files the template denies (file_denylist) are not listed. Works while the server is stopped or running. Refused with files_node_unreachable (the host is away; nothing is lost), server_busy (being created, removed, installed, moved or restored), files_server_suspended, files_busy (two requests per server and six per organisation at once) or files_unavailable (turned off), files_node_unsupported (the host's vexd does not report the file capability, so nothing is sent to it). Anything that changes files, and anything that reads file content (read, download, archive), is also refused with files_unavailable when the template's file denylist cannot be found for the server; only this listing (names and sizes) still works. A server of another organisation is not found. Needs games.files; no API token scope opens it. State: GF-1 is integrated on fakes only; it has not run against a real container node.

  • Permission: games.files
  • API token: not available, use a signed-in session
ParameterInNotes
pathquery
ResponseMeaning
200The folder
defaultError

GET /api/v1/orgs/{org}/games/{game}/files/archive

Download a file or folder of a game server as a zip

A zip of the file or folder at `path` ("" is the whole data directory) of at most 2000 entries, 16 MiB uncompressed and 2 MiB zipped (archive too large is file_too_large). Links, devices and denied files are left out. Recorded in Activity before any byte is sent.

  • Permission: games.files
  • API token: not available, use a signed-in session
ParameterInNotes
pathquery
ResponseMeaning
200The archive
defaultError

GET /api/v1/orgs/{org}/games/{game}/files/content

Read a text file of a game server

Returns the file as text for the editor, with the `version` to send back when saving. Refused with file_too_large (413, more than 1 MiB; `fields.size` is its size) and not_text_file (415: not valid UTF-8 or holds control bytes; download it instead). A folder, link or device is refused (file_wrong_type, file_special); a denied file is file_denied; a missing one file_not_found. Opening a file is recorded in Activity by path and size (never content) before the content is returned. See the list route for the other refusals.

  • Permission: games.files
  • API token: not available, use a signed-in session
ParameterInNotes
pathquery
ResponseMeaning
200The file
defaultError

PUT /api/v1/orgs/{org}/games/{game}/files/content

Save a text file of a game server

Writes the text atomically (a temporary file beside it, then a rename), so a failure leaves the original. Set `create` for a new file (refused with file_exists when it is there) or send the `version` that was read. A save with neither is refused (422): this route never overwrites a file it was not told the version of; a file that changed since is refused with file_conflict and nothing is written. The content must be text of at most 1 MiB (file_too_large, not_text_file). A write that would pass the plan's disk size is refused whole with file_disk_full. Recorded in Activity by path and size, never by content.

  • Permission: games.files
  • API token: not available, use a signed-in session

Request body (JSON):

{
  "content": "string",
  "create": false,
  "path": "string",
  "version": "string"
}
ResponseMeaning
200Saved
defaultError

POST /api/v1/orgs/{org}/games/{game}/files/delete

Delete a file or folder of a game server

Deletes a file or an empty folder. A folder with contents needs `recursive: true` (folder_not_empty otherwise); a folder holding a denied file is not deleted. The data directory itself cannot be deleted. There is no trash yet: a deleted file is gone (the 7-day restore is not built). Recorded in Activity by path.

  • Permission: games.files
  • API token: not available, use a signed-in session

Request body (JSON):

{
  "path": "string",
  "recursive": false
}
ResponseMeaning
204Done
defaultError

GET /api/v1/orgs/{org}/games/{game}/files/download

Download a file of a game server

The file's bytes as an attachment (never rendered, never sniffed, never cached), at most 2 MiB; bigger files go by SFTP (file_too_large). The download is recorded in Activity before any byte is sent.

  • Permission: games.files
  • API token: not available, use a signed-in session
ParameterInNotes
pathquery
ResponseMeaning
200The file
defaultError

POST /api/v1/orgs/{org}/games/{game}/files/mkdir

Create a folder in a game server

Creates one folder; its parent must exist (file_not_found) and it must not (file_exists).

  • Permission: games.files
  • API token: not available, use a signed-in session

Request body (JSON):

{
  "path": "string"
}
ResponseMeaning
201Created
defaultError

POST /api/v1/orgs/{org}/games/{game}/files/rename

Rename or move a file or folder of a game server

Moves a file or folder inside the data directory. It never replaces: a destination that exists is refused (file_exists). A folder cannot be moved into itself, and a folder holding a denied file is not moved.

  • Permission: games.files
  • API token: not available, use a signed-in session

Request body (JSON):

{
  "from_path": "string",
  "to_path": "string"
}
ResponseMeaning
200Moved
defaultError

PUT /api/v1/orgs/{org}/games/{game}/files/upload

Upload a file to a game server

The body is the raw file with Content-Type application/octet-stream, at most 2 MiB (file_too_large; bigger files go by SFTP). It never replaces a file unless `overwrite=true` is explicit (file_exists); with it the file is replaced whatever its version, which is the upload's meaning. The folder it goes in must exist. The same atomic write, quota and denylist rules as saving apply.

  • Permission: games.files
  • API token: not available, use a signed-in session
ParameterInNotes
pathquery
overwritequery
ResponseMeaning
201Stored
defaultError

GET /api/v1/orgs/{org}/games/{game}/mods

List the mods and plugins a game server's template allows

The allow-list from the server's signed template (`mods`), each item with its state in the mods folder: `available`, `installed` (the file is there with the pinned size) or `different` (a file of that name is there with another size, which an install will not replace). `available` is false when the template declares none. A customer never supplies a URL: an item is installed by its id.

  • Permission: games.files
  • API token: not available, use a signed-in session
ResponseMeaning
200The allow-list
defaultError

POST /api/v1/orgs/{org}/games/{game}/mods/install

Install an allow-listed mod or plugin on a game server

Downloads the item from the URL the signed template names (never one the customer gives), refuses it unless its length and SHA-256 are exactly the pinned ones (mod_checksum_mismatch, 502; mod_fetch_failed, 502, when the download did not complete), and writes it into the mods folder through the file manager's atomic write, creating the folder if needed. The file must not exist with other contents (mod_file_present, 409); installing what is already there is a no-op. Items are at most 2 MiB, the file manager's transfer limit. The download uses the SSRF-hardened client and happens before any slot on the node is taken. Recorded in Activity by mod id, file name, version and size. A restart is needed for the game to load it (`restart_required`).

  • Permission: games.files
  • API token: not available, use a signed-in session

Request body (JSON):

{
  "id": "string"
}
ResponseMeaning
200Installed, or already was
defaultError

POST /api/v1/orgs/{org}/games/{game}/mods/remove

Remove an allow-listed mod or plugin from a game server

Deletes the file the item put in the mods folder. Only an item that is `installed` (the pinned name and size) is removed; a file of the same name with another size is the customer's own and is left (mod_not_installed, 409). Recorded in Activity.

  • Permission: games.files
  • API token: not available, use a signed-in session

Request body (JSON):

{
  "id": "string"
}
ResponseMeaning
200Removed
defaultError

POST /api/v1/orgs/{org}/games/{game}/power

Start, stop, restart or kill a game server

Queues one power action for the node and answers 202 with an operation_id. Start works from offline or crashed; stop from running or starting; kill from running, starting or stopping, and it also works while a stop or start is in flight, which it supersedes; restart stops a running server and starts it again when the stop is confirmed, and starts one that is not running. Start and restart are refused with eula_required until the current EULA version for the game is accepted for the server (see the eula route), and with eula_unavailable when no text is published for the game; stop and kill are never refused for that. Refused with server_busy while another action runs on the server, and with server_not_active when the server is not active (still being set up, suspended or failed) or is in a state that cannot do that. The state shown changes at once; the node confirms it. A server of another organisation, or a VPS, is not found.

  • Permission: games.manage
  • API token: not available, use a signed-in session

Request body (JSON):

{
  "action": "start"
}
ResponseMeaning
202Queued
defaultError

POST /api/v1/orgs/{org}/games/{game}/reinstall

Reinstall a game server

Runs the game's installer again on a stopped server. Needs the server's name in confirm_name and a recent step-up. Reinstalling can replace game files and reset settings the installer owns: take a backup first (the server's own files are kept or replaced by the installer, never by this route). Refused with reinstall_needs_stopped unless the server is offline or crashed, server_busy while another action runs, server_not_active while it is still being set up, suspended or failed, and reinstall_unavailable when its host cannot reinstall yet (nothing is queued). The state shows installing at once and returns to offline when the host confirms; the outcome is reported in the server's reinstall field.

  • Permission: games.manage
  • API token: not available, use a signed-in session
  • Step-up: needs a recent password check

Request body (JSON):

{
  "confirm_name": "string"
}
ResponseMeaning
202Queued
defaultError

GET /api/v1/orgs/{org}/games/{game}/schedules

List a game server's schedules

The server's schedules, oldest first, with each one's next run, last run and its outcome (`last_status`: ok, failed, skipped or missed; `last_error_code`), the failures in a row, and what the plan allows. A schedule that fails `limits.failure_limit` runs in a row is switched off (`enabled` false) and says why in `last_error_code`; saving it again switches it back on. A server of another organisation is not found. Needs games.schedules; no API token scope opens it. State: integrated on fakes.

  • Permission: games.schedules
  • API token: not available, use a signed-in session
ResponseMeaning
200The schedules
defaultError

POST /api/v1/orgs/{org}/games/{game}/schedules

Create a schedule for a game server

Creates a schedule: a five-field cron expression (minute hour day-of-month month weekday; numbers, ranges, lists and steps) in an IANA time zone, and one to ten tasks run in order. A task is a power action (start, stop or restart), one console command line (up to 1,024 characters, sent as written) or a backup; each waits `offset_seconds` (0 to 3,600, at most six hours in all) after the one before finished, and the run stops at a failed task unless it sets `continue_on_failure`. A time that does not exist when clocks go forward runs once at the first moment after the gap; a repeated hour runs once. Runs must be at least five minutes apart. `missed_policy` says what happens if the worker was down at the due time: run_once makes the run up (once) if it is less than 30 minutes late, skip records it as missed; later than that it is always missed, never run. A server that is not active at run time skips the run and says so. A backup task while another backup is running is skipped with the reason backup_running. A schedule may hold only a task its author could do by hand (power and commands need games.manage, backups games.backups): refused with schedule_task_forbidden, and a task whose author has lost the permission fails when it runs (permission_revoked). The plan allows a number of schedules (schedule_limit_reached, schedules_not_included) and names are unique per server, ignoring case (schedule_name_taken). Needs games.schedules. State: integrated on fakes; nothing has run on a real node.

  • Permission: games.schedules
  • API token: not available, use a signed-in session

Request body (JSON):

{
  "cron": "0 4 * * *",
  "enabled": false,
  "missed_policy": "run_once",
  "name": "string",
  "tasks": [
    null
  ],
  "tz": "Europe/Berlin"
}
ResponseMeaning
201Created
defaultError

DELETE /api/v1/orgs/{org}/games/{game}/schedules/{schedule}

Delete a game server schedule

Switches the schedule off and removes it from the list; its past runs stay in the history the audit trail points to. A run in flight finishes. A schedule of another server or organisation is not found. Needs games.schedules.

  • Permission: games.schedules
  • API token: not available, use a signed-in session
ResponseMeaning
204Deleted
defaultError

PUT /api/v1/orgs/{org}/games/{game}/schedules/{schedule}

Change a game server schedule

Replaces the schedule's definition (the same fields and rules as creating one). Saving clears the failures in a row and switches a schedule that was switched off back on (when `enabled` is true), and makes the person who saved it the one it acts as: its tasks run with the permissions that person has when they run. A run in flight finishes with the tasks it started with, or fails as schedule_changed if the list changed under it. A schedule of another server or organisation is not found. Needs games.schedules.

  • Permission: games.schedules
  • API token: not available, use a signed-in session

Request body (JSON):

{
  "cron": "0 4 * * *",
  "enabled": false,
  "missed_policy": "run_once",
  "name": "string",
  "tasks": [
    null
  ],
  "tz": "Europe/Berlin"
}
ResponseMeaning
200Saved
defaultError

GET /api/v1/orgs/{org}/games/{game}/schedules/{schedule}/runs

List the runs of a game server schedule

The latest 20 runs, newest first, each with its due time, how it ended (running, ok, failed, skipped or missed, with `reason` such as server_not_active, worker_down, missed_policy or task_failed) and the outcome of each task (pending, running, ok, failed or skipped, with a curated `code`: not_running, server_busy, eula_required, permission_revoked, backup_running, backup_limit_reached, timeout and others). No operation or backup ids are shown. A schedule of another server or organisation is not found. Needs games.schedules.

  • Permission: games.schedules
  • API token: not available, use a signed-in session
ResponseMeaning
200The runs
defaultError

POST /api/v1/orgs/{org}/games/{game}/sftp

Get a short-lived SFTP credential for a game server

Issues a credential for this person, from this panel session, for this one server on its node: the user name encodes the server and the person, and the password is a signed token that is good for 10 minutes to log in (a session that began in time lasts as long as the person's right to the server). It is shown only here and stored nowhere, so there is nothing to look up; ask for another. Account passwords never reach a node. Refused with sftp_unavailable while SFTP is turned off, files_node_unreachable, server_busy or files_server_suspended. State: GF-1 issues the credential on fakes; the SFTP service that accepts it (vex-sftp) is not built.

  • Permission: games.files
  • API token: not available, use a signed-in session
  • Session only: a person must be present

Request body (JSON):

{}
ResponseMeaning
201Issued
defaultError

GET /api/v1/orgs/{org}/games/{game}/usage

CPU, memory, disk, network and players of a game server

One series per metric over a time range, downsampled to the step, in the shape of a VPS server's usage. Samples are kept at one-minute resolution for 30 days. A bucket with no sample is a gap: its value and max are null and samples is 0. It is never zero. A gap can mean the server was off, the node was unreachable, or the reading was not available: the node reads CPU against the server's own cap (cpu.max) and memory from its cgroup, the disk in use from its data directory (every ten minutes, and not at all for a directory too large to measure cheaply), network at the server's routed interface once the game network exists on the node, and players only from a game that answers a query (Minecraft Java). Network is bytes per second. Buckets start at multiples of the step since the Unix epoch (UTC); the answer covers whole buckets. Value is the mean over the minutes that have a sample; max is the largest per-minute value. A server of another org is a 404. Needs games.read; no API token scope opens it. State: integrated on fakes; no real node has measured a real container.

  • Permission: games.read
  • API token: not available, use a signed-in session
ParameterInNotes
metricqueryA metric to return. Repeat or comma-separate. Every game metric when omitted
fromqueryStart of the range (RFC 3339). Default is 24 hours before to
toqueryEnd of the range (RFC 3339)
stepqueryBucket size in seconds, a multiple of 60 up to 86400. Default is the smallest that gives at most 600 points
ResponseMeaning
200Usage series
defaultError

health

GET /api/v1/health

Liveness check

Liveness check. Returns status ok when the API process is running. Needs no sign-in.

  • Access: public, no authentication
ResponseMeaning
200OK

invites

POST /api/v1/invites/accept

Accept an organisation invite

Accepts an organisation invite with the token from the invite email, and adds you with the invited role. The token must match your own email address; otherwise it returns token_invalid.

  • API token: not available, use a signed-in session

Request body (JSON):

{
  "token": "string"
}
ResponseMeaning
200Joined
defaultError

GET /api/v1/orgs/{org}/invites

List pending invites

Lists pending invites with the invited email, role and expiry.

  • Permission: org.members.manage
  • API token: not available, use a signed-in session
ResponseMeaning
200OK
defaultError

POST /api/v1/orgs/{org}/invites

Invite a member

Invites an email address to the organisation as admin, member, billing or viewer (not owner), and emails the invite link. You can only invite into a role whose permissions you hold in full (403 role_escalation), and that is checked again when the invite is accepted. Personal organisations cannot have invites (personal_org).

  • Permission: org.members.manage
  • API token: not available, use a signed-in session

Request body (JSON):

{
  "email": "string",
  "role": "admin"
}
ResponseMeaning
201Invited
defaultError

DELETE /api/v1/orgs/{org}/invites/{invite}

Revoke an invite

Revokes a pending invite so its link can no longer be accepted. Returns not_found if the invite does not exist in this organisation.

  • Permission: org.members.manage
  • API token: not available, use a signed-in session
ResponseMeaning
204Done
defaultError

invoices

GET /api/v1/orgs/{org}/invoices

List invoices

Newest first. With status unpaid or all, open renewals appear as rows of kind proforma (pay them with POST /proformas/{id}/pay).

  • Permission: billing.read
  • API token: not available, use a signed-in session
ParameterInNotes
statusquery
beforequeryThe next_before value of the previous page
limitquery
ResponseMeaning
200Invoices
defaultError

GET /api/v1/orgs/{org}/invoices/{invoice}

Get an invoice

Returns one of the organisation's invoices with its lines, tax and the part paid from credit. Returns not_found if the invoice belongs to another organisation.

  • Permission: billing.read
  • API token: not available, use a signed-in session
ResponseMeaning
200Invoice
defaultError

GET /api/v1/orgs/{org}/invoices/{invoice}.pdf

Download an invoice as PDF

Rendered only from the immutable invoice (its lines and frozen snapshot). Another org's invoice is a 404.

  • Permission: billing.read
  • API token: not available, use a signed-in session
ResponseMeaning
200The invoice as a PDF attachment
defaultError

isos

GET /api/v1/orgs/{org}/isos

List the organisation's ISOs and the system library

The organisation's own ISOs in any state, then the ready ISOs of the read-only system library, with the library's usage against its quota. ISOs are never shared between organisations. With arch, only ISOs built for that CPU architecture are listed (what a server of that architecture can mount); without it every ISO is listed, each with its arch.

  • Permission: servers.read
  • API token: not available, use a signed-in session
ParameterInNotes
archquery
ResponseMeaning
200Library
defaultError

POST /api/v1/orgs/{org}/isos

Fetch an ISO by URL into the library

Queues a fetch. The URL must be https on port 443 of a public host with no credentials in it; every address the fetch connects to must be public too. Redirects are not followed, so a URL that redirects fails with redirect_not_allowed (give the final address). The fetch is capped at the space left in the quota and at 10 GiB, stops after 90 minutes, and is aborted (too_slow) if it moves less than 64 KiB/s over a whole minute; an interrupted fetch starts again from scratch up to 3 times. An organisation may have 3 fetches waiting or running at once; a fourth is refused with 429 iso_fetch_queue_full. If sha256 is given the bytes must match it. Poll the list for the outcome.

  • Permission: iso.manage
  • API token: not available, use a signed-in session

Request body (JSON):

{
  "arch": "x86_64",
  "name": "string",
  "sha256": "string",
  "url": "string"
}
ResponseMeaning
202Queued
defaultError

POST /api/v1/orgs/{org}/isos/upload

Upload an ISO into the library

The body is the raw image, with Content-Type application/octet-stream and a Content-Length, which is reserved against the quota before any byte is read (iso_quota_exceeded otherwise). The body must be exactly that long and an ISO 9660 or UDF image, and must match sha256 if it is given. A stalled upload is cut off after a minute without data.

  • Permission: iso.manage
  • API token: not available, use a signed-in session
ParameterInNotes
namequery
sha256query
archqueryThe CPU architecture the ISO boots on; a server can mount only ISOs of its own architecture.
ResponseMeaning
201Stored
defaultError

DELETE /api/v1/orgs/{org}/isos/{iso}

Delete an ISO from the library

Removes one of the organisation's own ISOs and its bytes. Refused with iso_in_use while a server has it mounted or a mount of it is queued. System ISOs can't be deleted here.

  • Permission: iso.manage
  • API token: not available, use a signed-in session
ResponseMeaning
204Deleted
defaultError

limit-requests

POST /api/v1/orgs/{org}/limit-requests

Ask for a different limit

One request per limit can be open (409 limit_request_open otherwise). It reaches the staff queue; automatic approval does not exist. 422 when the number is the current limit.

  • Permission: org.manage
  • API token: not available, use a signed-in session

Request body (JSON):

{
  "limit": "servers",
  "reason": "string",
  "requested": 0
}
ResponseMeaning
201The request
defaultError

limits

GET /api/v1/orgs/{org}/limits

The organisation's limits, its use and its requests

Every limit with what the organisation uses now and whether staff changed it from the default, then the organisation's limit requests, newest first.

  • Permission: org.read
  • API token: not available, use a signed-in session
ResponseMeaning
200Limits and requests
defaultError

marketplace

POST /api/v1/orgs/{org}/marketplace/installs

Install a marketplace app on a new server

Places an order for a new server whose first-boot configuration is the chosen app, filled with the inputs. Needs what ordering a server needs: a verified email (email_unverified), the server limit not reached, capacity (no_capacity) and a plan at least as large as the app's minimums (validation_failed on price_id). Input problems are validation_failed on inputs. Values for password inputs that were left blank are generated and sealed; collect them once with the reveal call. A repeat with the same Idempotency-Key returns the same install. Returns 201 with the install, the order and a client_secret to confirm payment when one is needed. Answers 503 tax_unavailable while tax cannot be worked out yet for the buyer's location; nothing is created or charged, and trying again later succeeds once it is set up.

  • Permission: servers.manage
  • API token: not available, use a signed-in session
ParameterInNotes
Idempotency-Keyheader

Request body (JSON):

{
  "hostname": "string",
  "image_id": "00000000-0000-0000-0000-000000000000",
  "price_id": "00000000-0000-0000-0000-000000000000",
  "recipe_id": "string",
  "region": "string",
  "ssh_key_ids": [
    null
  ]
}
ResponseMeaning
201Install and order placed
defaultError

GET /api/v1/orgs/{org}/marketplace/installs/{install}

Show an install and its progress

The app and version, the order and server it produced, whether the credentials were already shown, and the install state the server reported: pending until the server exists and reports, then installing, ready or failed. The state comes from the customer's own server, so treat it as a hint. An install of another organisation is not found.

  • Permission: servers.read
  • API token: not available, use a signed-in session
ResponseMeaning
200Install
defaultError

POST /api/v1/orgs/{org}/marketplace/installs/{install}/credentials/reveal

Show an install's generated credentials once

Returns the values the control plane generated for this install and marks them shown. The first call gets them (Cache-Control: no-store); every later call is 409 credentials_viewed. Recorded in Activity by key count, never by value. Closed to API tokens.

  • Permission: servers.manage
  • API token: not available, use a signed-in session
ResponseMeaning
200The credentials
defaultError

GET /api/v1/orgs/{org}/marketplace/recipes

List marketplace apps

The signed one-click apps that can be installed on a new server, ordered by id, each with the inputs its form needs and the smallest plan it runs on. Only recipes whose signature verified are listed, so the list is empty until the owner has signed one. Answers not_found when the marketplace is switched off.

  • Permission: servers.read
  • API token: not available, use a signed-in session
ResponseMeaning
200Apps
defaultError

GET /api/v1/orgs/{org}/marketplace/recipes/{recipe}

Show a marketplace app

One app with its inputs and minimums. The template is never returned. An id that is not in the catalog is not found.

  • Permission: servers.read
  • API token: not available, use a signed-in session
ResponseMeaning
200App
defaultError

me

GET /api/v1/me

Current user

Returns the signed-in user: profile, whether email is verified, authenticator and passkey status, staff permissions, the time until which a step-up is valid, and any active impersonation.

  • API token: not available, use a signed-in session
ResponseMeaning
200User
defaultError

PATCH /api/v1/me

Update profile

Changes your display name (up to 80 characters).

  • API token: not available, use a signed-in session

Request body (JSON):

{
  "name": "string"
}
ResponseMeaning
204Done
defaultError

GET /api/v1/me/audit

Your account activity

Lists activity on your own account, newest first. Pages with before and limit (default 50, at most 100).

  • API token: not available, use a signed-in session
ParameterInNotes
beforequery
limitquery
ResponseMeaning
200Events
defaultError

DELETE /api/v1/me/deletion

Cancel a request to delete your account

Stops a pending request. Nothing has been erased during the grace period, so it needs no step-up. You are emailed. 404 when nothing is pending.

  • API token: not available, use a signed-in session
  • Session only: a person must be present
ResponseMeaning
204Done
defaultError

GET /api/v1/me/deletion

Whether your account can be deleted, and any request

The pending request if there is one, the last one that ended, and what is in the way now (servers, money owed either way, disputes, holds, unpaid invoices, an organisation you solely own that has other members). `allowed` is true when nothing is.

  • API token: not available, use a signed-in session
  • Session only: a person must be present
ResponseMeaning
200The state
defaultError

POST /api/v1/me/deletion

Ask for your account to be deleted

Confirm with your sign-in email. Refused with 409 deletion_blocked, naming each reason in `fields`, while anything is in the way. Otherwise the request waits out the grace period (14 days by default), during which you can cancel, and is then carried out: your credentials, cards, SSH keys, API tokens and support conversations are deleted or anonymised; invoices, payments and tax records stay. Needs a recent step-up and a panel session. You are emailed.

  • API token: not available, use a signed-in session
  • Step-up: needs a recent password check
  • Session only: a person must be present

Request body (JSON):

{
  "confirm": "string"
}
ResponseMeaning
201The request
defaultError

DELETE /api/v1/me/email-change

Cancel a waiting change of sign-in address

Withdraws the change waiting for confirmation. Its links stop working. 404 when none is waiting.

  • API token: not available, use a signed-in session
  • Session only: a person must be present
ResponseMeaning
204Cancelled
defaultError

GET /api/v1/me/email-change

Your sign-in address and any change waiting for confirmation

Your current sign-in address and, if you asked to change it and the new address has not confirmed yet, the address and when the request runs out. Panel session only.

  • API token: not available, use a signed-in session
  • Session only: a person must be present
ResponseMeaning
200The address and any waiting change
defaultError

POST /api/v1/me/email-change

Ask to change your sign-in email address

Needs a recent step-up and a panel session. Sends a confirmation link to the NEW address and a notice with a cancel link to the OLD one; nothing changes until the new address opens its link, within 24 hours. The answer is the same whether or not the new address already belongs to an account (no link is sent in that case, and the change can never be confirmed), so it cannot be used to find out who has one. A newer request replaces a waiting one. At most five an hour. When the change is confirmed every session is signed out and unused email links are voided; passkeys and the authenticator app stay.

  • API token: not available, use a signed-in session
  • Step-up: needs a recent password check
  • Session only: a person must be present

Request body (JSON):

{
  "new_email": "string"
}
ResponseMeaning
202Check both inboxes
defaultError

GET /api/v1/me/export-downloads/{token}

Download an export with a link

Serves the zip for a valid link made for the signed-in person. An expired, altered or someone else's link is token_invalid. Sent with Cache-Control no-store; each download is audited.

  • API token: not available, use a signed-in session
  • Session only: a person must be present
ResponseMeaning
200The archive
defaultError

GET /api/v1/me/exports

Your data exports

Your own account exports, newest first, with their state and how many days a finished one can be downloaded. Panel session only.

  • API token: not available, use a signed-in session
  • Session only: a person must be present
ResponseMeaning
200The exports
defaultError

POST /api/v1/me/exports

Ask for a copy of your data

Queues an export of the personal data held about you (profile, organisations, sessions, support tickets, SSH keys, API token metadata, your own activity) as a zip of JSON files. It never holds a secret or another person's data. The worker builds it and emails you when it is ready. Needs a recent step-up, a panel session, and at most three a day; one at a time (export_open).

  • API token: not available, use a signed-in session
  • Step-up: needs a recent password check
  • Session only: a person must be present
ResponseMeaning
202Queued
defaultError

GET /api/v1/me/exports/{export}

One data export

State, size, checksum and expiry of one of your exports. 404 for anyone else's.

  • API token: not available, use a signed-in session
  • Session only: a person must be present
ResponseMeaning
200The export
defaultError

POST /api/v1/me/exports/{export}/download-link

Get a short-lived download link for an export

Needs a recent step-up. Makes a link that works for five minutes, once, for you only in this session, and while the export is ready (export_not_ready otherwise). Making another link cancels the earlier one.

  • API token: not available, use a signed-in session
  • Step-up: needs a recent password check
  • Session only: a person must be present
ResponseMeaning
200The link
defaultError

GET /api/v1/me/game-access

The game servers shared with me and the invitations waiting

The signed-in person's active grants and the invitations sent to their address that are still open (not expired), across organisations. Each says which server, whose organisation, which rights, and who invited them. Nothing else about the organisation is shown. State: integrated on fakes.

  • API token: not available, use a signed-in session
ResponseMeaning
200Access given to me
defaultError

DELETE /api/v1/me/game-access/{grant}

Decline an invitation or give up access to a game server

The person's own act, with no step-up: a pending invitation is declined (game.subuser_declined), an active grant ends (game.subuser_left) and a console they have open ends at the gateway's next heartbeat. Another person's grant is not found. State: integrated on fakes.

  • API token: not available, use a signed-in session
ResponseMeaning
204Done
defaultError

POST /api/v1/me/game-access/{grant}/accept

Accept an invitation to a game server

Accepts a pending invitation sent to the signed-in person's verified address. Another account, an expired or withdrawn invitation and an unverified address are all invite_invalid (404). The inviter's rights are checked again now (invite_inviter_changed, nothing is accepted); that includes the inviter still being an active member whose role holds games.subusers, checked under the organisation lock. Audited as game.subuser_accepted. State: integrated on fakes.

  • API token: not available, use a signed-in session
ResponseMeaning
200Accepted
defaultError

GET /api/v1/me/mail-status

Whether email to your address is stopped

True when email to your sign-in address bounced for good or was marked as spam, so it is no longer sent.

  • API token: not available, use a signed-in session
ResponseMeaning
200Mail status of your sign-in address
defaultError

GET /api/v1/me/notification-preferences

Which emails you receive

Every kind of email, whether it is required (`locked`) and whether you receive it. Security and billing email is locked on.

  • API token: not available, use a signed-in session
ResponseMeaning
200The preferences
defaultError

PUT /api/v1/me/notification-preferences

Choose which optional emails you receive

Names the kinds to change and leaves the others as they are. Asking to switch off a locked kind (security, billing) is refused with 422 and nothing changes. Takes effect for email queued from now on.

  • API token: not available, use a signed-in session

Request body (JSON):

{
  "preferences": {}
}
ResponseMeaning
200The preferences after the change
defaultError

GET /api/v1/me/orgs

Organisations you belong to

Lists the organisations you belong to with your role in each. A viewing session started by staff sees only the organisation it was opened for.

  • API token: not available, use a signed-in session
ResponseMeaning
200Orgs
defaultError

GET /api/v1/me/passkeys

List passkeys

Lists your passkeys with their names, creation time and last use.

  • API token: not available, use a signed-in session
ResponseMeaning
200OK
defaultError

POST /api/v1/me/passkeys/begin

Start adding a passkey

Starts adding a passkey and returns the WebAuthn options with a challenge_id. Needs a recent step-up.

  • API token: not available, use a signed-in session
  • Step-up: needs a recent password check
ResponseMeaning
200Options
defaultError

POST /api/v1/me/passkeys/finish

Finish adding a passkey

Finishes adding a passkey from the browser's WebAuthn response and the challenge_id from passkeys/begin. The optional name query parameter labels it. Needs a recent step-up.

  • API token: not available, use a signed-in session
  • Step-up: needs a recent password check
ParameterInNotes
X-Vexelia-Challengeheader
namequery

Request body (JSON):

{}
ResponseMeaning
201Added
defaultError

DELETE /api/v1/me/passkeys/{passkey}

Remove a passkey

Removes one of your passkeys. Needs a recent step-up. Returns not_found if the passkey is not yours.

  • API token: not available, use a signed-in session
  • Step-up: needs a recent password check
ParameterInNotes
passkeypath
ResponseMeaning
204Done
defaultError

POST /api/v1/me/password

Change password

Changes your password and signs out all your other sessions. Needs the current password (invalid_credentials) and a new one that meets the password policy (weak_password: length) and is not a very common password or one found in a known breach (password_breached; the breach-list lookup lets the password through when that service is down). A notice is emailed.

  • API token: not available, use a signed-in session

Request body (JSON):

{
  "current_password": "string",
  "new_password": "string"
}
ResponseMeaning
204Done
defaultError

POST /api/v1/me/recovery-codes

Regenerate recovery codes

Replaces your recovery codes with a new set, shown once. Needs a recent step-up. Returns conflict if the authenticator app is not set up.

  • API token: not available, use a signed-in session
  • Step-up: needs a recent password check
ResponseMeaning
200Codes
defaultError

GET /api/v1/me/sessions

List signed-in sessions

Lists your signed-in sessions with IP address, browser and last activity, and marks the current one.

  • API token: not available, use a signed-in session
ResponseMeaning
200OK
defaultError

DELETE /api/v1/me/sessions/{session}

Sign out a session

Signs out one of your own sessions, which can be the current one (the session cookie is then cleared). Returns not_found if the session is not yours.

  • API token: not available, use a signed-in session
ParameterInNotes
sessionpath
ResponseMeaning
204Done
defaultError

GET /api/v1/me/sso

Your linked identity providers

The providers on offer with whether each is linked to your account, and links to providers that are no longer offered (so they can still be removed). Also whether this is a staff account (which cannot use a provider) and whether you can sign in with a password. Never the provider's subject. Panel session only.

  • API token: not available, use a signed-in session
  • Session only: a person must be present
ResponseMeaning
200Your links
defaultError

DELETE /api/v1/me/sso/{provider}

Unlink an identity provider from your account

Needs a recent step-up and a panel session. Refused with sso_last_sign_in_method when it is the only way you can sign in (no password you can use, no passkey, no other provider that is switched on); the check runs under a lock on your account, so two unlinks at once cannot remove both. A mail tells you it happened.

  • API token: not available, use a signed-in session
  • Step-up: needs a recent password check
  • Session only: a person must be present
ResponseMeaning
204Unlinked
defaultError

POST /api/v1/me/sso/{provider}/link

Start linking an identity provider to your account

Needs a recent step-up and a panel session. Returns the provider's authorization address; the provider asks you to sign in again (prompt=login) and the callback must return in this browser and this session. The identity is tied to this account whatever its email says, and only if no other account has it. Refused for staff accounts (sso_not_for_staff) and when this provider is already linked (sso_already_linked). A mail tells you it happened.

  • API token: not available, use a signed-in session
  • Step-up: needs a recent password check
  • Session only: a person must be present
ResponseMeaning
200Where to send the browser
defaultError

POST /api/v1/me/step-up

Re-authenticate for sensitive actions

Re-authenticates with your password or authenticator code and returns the time until which sensitive actions are allowed (5 minutes). Wrong credentials return invalid_credentials.

  • API token: not available, use a signed-in session

Request body (JSON):

{
  "code": "string",
  "password": "string"
}
ResponseMeaning
200Window open
defaultError

DELETE /api/v1/me/totp

Remove authenticator app

Turns off authenticator-app sign-in and deletes your recovery codes. Needs a recent step-up. A confirmation email is queued to your address.

  • API token: not available, use a signed-in session
  • Step-up: needs a recent password check
ResponseMeaning
204Done
defaultError

POST /api/v1/me/totp

Start authenticator app setup

Starts authenticator-app setup and returns the secret and otpauth URL. Needs a recent step-up. Returns conflict if an authenticator app is already confirmed. Setup completes with totp/confirm.

  • API token: not available, use a signed-in session
  • Step-up: needs a recent password check
ResponseMeaning
200Secret
defaultError

POST /api/v1/me/totp/confirm

Confirm authenticator app and get recovery codes

Confirms authenticator-app setup with a current code and returns your recovery codes, shown once. Needs a recent step-up. A wrong code returns invalid_code.

  • API token: not available, use a signed-in session
  • Step-up: needs a recent password check

Request body (JSON):

{
  "code": "string"
}
ResponseMeaning
200Codes
defaultError

GET /api/v1/me/transfers

Ownership offers made to you

Lists the open offers to take over an organisation that were made to you, with what you would take on: how many servers it has and how many payment methods stay on it. Expired offers are not listed.

  • API token: not available, use a signed-in session
ResponseMeaning
200Offers
defaultError

POST /api/v1/me/transfers/{transfer}/accept

Accept an ownership offer

Makes you the owner and the person who handed it over the role they chose, in one step. Needs a recent step-up and accept_billing: true, confirming you take over the organisation's billing: its renewals charge the payment methods and credit already on it until you change them. An offer that is expired, answered, or no longer the giver's to make returns 409 transfer_invalid. Someone else's offer is 404.

  • API token: not available, use a signed-in session
  • Step-up: needs a recent password check

Request body (JSON):

{
  "accept_billing": false
}
ResponseMeaning
200Accepted
defaultError

POST /api/v1/me/transfers/{transfer}/decline

Decline an ownership offer

Turns the offer down; the owner is told and nothing changes.

  • API token: not available, use a signed-in session
ResponseMeaning
204Done
defaultError

DELETE /api/v1/me/trusted-browsers

Remove every trusted browser

Removes all of your trusted browsers, so the authenticator code is asked at every password sign-in again, and clears this browser's trusted-browser cookie. Needs no step-up: it only takes a convenience away. Audited when anything was removed. Panel session only.

  • API token: not available, use a signed-in session
  • Session only: a person must be present
ResponseMeaning
204Done
defaultError

GET /api/v1/me/trusted-browsers

Browsers you marked as trusted

Lists the browsers you marked as trusted when you signed in with your authenticator app code, newest first, with the browser name, the address it was marked from, when it was last used to skip the code and when the trust ends; the one you are using is marked current. enabled is false when the service has turned the feature off, and days is how long a new trust lasts. Panel session only.

  • API token: not available, use a signed-in session
  • Session only: a person must be present
ResponseMeaning
200The trusted browsers
defaultError

DELETE /api/v1/me/trusted-browsers/{browser}

Remove one trusted browser

Removes one of your trusted browsers; the next password sign-in from it asks for the authenticator code. If it is the browser making the request, its trusted-browser cookie is cleared too. Returns not_found if it is not yours. Needs no step-up. Audited. Panel session only.

  • API token: not available, use a signed-in session
  • Session only: a person must be present
ResponseMeaning
204Done
defaultError

GET /api/v1/me/two-factor

Your second factor and which organisations require one

Whether you have a second factor (a confirmed authenticator app or a passkey), how many recovery codes are left, and which of your organisations require one, with each grace period. enrolment_required is true when you have none and a rule already holds you out; signin_required when you have one but this session did not use it (a password-only session of a person with a passkey is not compliant) (an organisation whose grace period ended, or the staff rule): turn one on before anything else works. must_keep says removing your last one would be refused. Panel session only. No secret is returned.

  • API token: not available, use a signed-in session
  • Session only: a person must be present
ResponseMeaning
200Your state
defaultError

members

GET /api/v1/orgs/{org}/members

List members

Lists the organisation's members with email, name, role and the date they joined.

  • Permission: org.members.read
  • API token: not available, use a signed-in session
ResponseMeaning
200OK
defaultError

DELETE /api/v1/orgs/{org}/members/{user}

Remove a member

Removes a member from the organisation (not one who holds more than you do: 403 member_outranks), and revokes the API tokens they created and the support access they granted here. Needs a recent step-up. Only an owner may remove an owner, the last owner cannot be removed (last_owner), and a personal organisation has no removable members (personal_org).

  • Permission: org.members.manage
  • API token: not available, use a signed-in session
  • Step-up: needs a recent password check
ResponseMeaning
204Done
defaultError

PATCH /api/v1/orgs/{org}/members/{user}

Change a member's role

Changes a member's role to owner, admin, member, billing or viewer (role), or to one of the organisation's own roles (role_id), not both. Needs a recent step-up. You can only give a role whose permissions you hold in full (403 role_escalation) and only change a member who holds nothing you do not (403 member_outranks). Only an owner may grant or remove the owner role, the last owner cannot be demoted (last_owner), and a personal organisation's roles cannot be changed (personal_org).

  • Permission: org.members.manage
  • API token: not available, use a signed-in session
  • Step-up: needs a recent password check

Request body (JSON):

{
  "role": "owner",
  "role_id": "00000000-0000-0000-0000-000000000000"
}
ResponseMeaning
204Done
defaultError

operations

GET /api/v1/orgs/{org}/operations

List server actions

Lists the actions (power, rebuild, destroy and the like) run on the organisation's servers, newest first with the id as tiebreak, 50 a page unless limit says otherwise (1 to 200). Pass server to see one server's actions. Cursor paged: send next_cursor back as cursor; it is null on the last page. An invalid cursor, limit or server is 422. Follow one action at GET /orgs/{org}/operations/{operation}.

  • Permission: servers.read
  • API token scope: servers:read
ParameterInNotes
serverqueryOnly this server's actions
limitqueryPage size, 1 to 200 (a larger value is lowered to 200). Without limit and cursor the whole list is returned, except where the operation says otherwise.
cursorqueryThe next_cursor of the previous page. Opaque; valid only for the list that issued it.
ResponseMeaning
200Actions
defaultError

Example:

curl -X GET "$VEXELIA_URL/api/v1/orgs/$org/operations" \
  -H "Authorization: Bearer $VEXELIA_TOKEN"

GET /api/v1/orgs/{org}/operations/{operation}

Progress of a server action

Returns one operation (power, rebuild, rescue, firewall apply) with its status: queued, running, succeeded or failed, and an error_code when it failed. Operation ids come from the 202 answers of the calls that queue work.

  • Permission: servers.read
  • API token scope: servers:read
ResponseMeaning
200Operation
defaultError

Example:

curl -X GET "$VEXELIA_URL/api/v1/orgs/$org/operations/$operation" \
  -H "Authorization: Bearer $VEXELIA_TOKEN"

orders

POST /api/v1/orgs/{org}/orders

Order a server

Orders a server: price, region, image, hostname and at least one SSH key, with optional user data and automatic renewal. Needs a verified email (email_unverified). Refused when the organisation's server limit is reached or no node has capacity (no_capacity). The plan and the image must be of the same CPU architecture (x86_64 or aarch64): an order that mixes them is a validation_failed on image_id saying which to choose. A dedicated plan is placed only on a host that reports it pins servers to cores of their own and has whole cores free; where none does, the order is a validation_failed on price_version_id (no free dedicated cores in this region). The catalog's regions[].offers say which architectures and which dedicated cores each region can place. Returns 201 with the order and a client_secret to confirm payment when one is needed. With restore_backup_id the order is for a new server restored from one of the organisation's backups: it is a purchase like any other (it counts against the server limit and is paid in full), the original server is not touched, and if the backup is gone when payment arrives the order is refunded and no server is built. An order with restore_backup_id needs a recent step-up (403 step_up_required without one), because it copies a whole disk onto a server the given keys can log in to; an ordinary order needs none, and a token is never allowed to order. Answers 503 tax_unavailable while tax cannot be worked out yet for the buyer's location; nothing is created or charged, and trying again later succeeds once it is set up. When the platform runs sync hook plugins, an order can be refused with 403 request_declined, 409 request_declined_limit or 403 request_declined_region, in the platform's own words, before anything is created; a retry of an order that already exists is answered by that order. A plugin may lower the price within a cap (at most 10% and 500.00); the order then shows the amount as discount_minor, and the invoice and the payment are for exactly the order's total, priced by the platform. A plugin that fails or is slow is ignored.

  • Permission: servers.manage
  • API token: not available, use a signed-in session
ParameterInNotes
Idempotency-Keyheader

Request body (JSON):

{
  "hostname": "string",
  "price_id": "00000000-0000-0000-0000-000000000000",
  "region": "string",
  "ssh_key_ids": [
    null
  ]
}
ResponseMeaning
201Order priced; pay with client_secret
defaultError

GET /api/v1/orgs/{org}/orders/{order}

Get an order

Returns an order and, once the server exists, its server id. Poll it after placing an order in the panel.

  • Permission: servers.read
  • API token scope: servers:read
ResponseMeaning
200Order
defaultError

Example:

curl -X GET "$VEXELIA_URL/api/v1/orgs/$org/orders/$order" \
  -H "Authorization: Bearer $VEXELIA_TOKEN"

orgs

POST /api/v1/orgs

Create an organisation

Creates an organisation with you as its owner.

  • API token: not available, use a signed-in session

Request body (JSON):

{
  "name": "string"
}
ResponseMeaning
201Created
defaultError

GET /api/v1/orgs/{org}

Organisation details

Returns the organisation the token belongs to: its name and your role in it.

  • Permission: org.read
  • API token scope: org:read
ResponseMeaning
200Org
defaultError

Example:

curl -X GET "$VEXELIA_URL/api/v1/orgs/$org" \
  -H "Authorization: Bearer $VEXELIA_TOKEN"

PATCH /api/v1/orgs/{org}

Rename organisation

Renames the organisation.

  • Permission: org.manage
  • API token: not available, use a signed-in session

Request body (JSON):

{
  "name": "string"
}
ResponseMeaning
204Done
defaultError

payment-methods

GET /api/v1/orgs/{org}/payment-methods

List saved cards

Lists the organisation's saved cards.

  • Permission: billing.read
  • API token: not available, use a signed-in session
ResponseMeaning
200Saved cards, default first
defaultError

POST /api/v1/orgs/{org}/payment-methods/setup

Start saving a card

Starts saving a card and returns a client secret and the provider's public key for the browser to confirm the card.

  • Permission: billing.manage
  • API token: not available, use a signed-in session
ResponseMeaning
200A setup intent. The card is stored only when the provider confirms it.
defaultError

DELETE /api/v1/orgs/{org}/payment-methods/{method}

Remove a card

Refused with autopay_needs_card (fields.servers lists them) when it is the last card and automatic renewal is on.

  • Permission: billing.manage
  • API token: not available, use a signed-in session
  • Step-up: needs a recent password check
ResponseMeaning
204Done
defaultError

POST /api/v1/orgs/{org}/payment-methods/{method}/default

Make a card the default

Makes a saved card the organisation's default. Returns not_found if the card is not one of this organisation's.

  • Permission: billing.manage
  • API token: not available, use a signed-in session
ResponseMeaning
204Done
defaultError

private-networks

GET /api/v1/orgs/{org}/private-networks

Every private network of the organisation, with its servers and limits

The organisation's private networks (the Private networks page), newest first, each with the servers attached to it (and the ones being attached or taken off), the private networks limit and servers, the active servers a network can be made for or attached to, each with an opaque location label. A private network is a layer-2 segment the organisation owns in one location, with a range the customer chose (RFC 1918, /16 to /24); its servers reach each other on it with private addresses and nothing else can: another organisation's servers are never on it. It is not billed. Deleted networks are not listed.

  • Permission: servers.read
  • API token scope: servers:read
ResponseMeaning
200Private networks, limits and eligible servers
defaultError

Example:

curl -X GET "$VEXELIA_URL/api/v1/orgs/$org/private-networks" \
  -H "Authorization: Bearer $VEXELIA_TOKEN"

POST /api/v1/orgs/{org}/private-networks

Create a private network

Creates a private network named name with the range cidr (an IPv4 network written with its network address, such as 10.20.0.0/24: inside 10.0.0.0/8, 172.16.0.0/12 or 192.168.0.0/16, and between /16 and /24) in the location of the server server_id (which only says where: nothing is attached). It costs nothing and touches no host. The range may not overlap another live network of the organisation (another organisation can use the same one: separate segments). The first host address of the range is kept (a gateway may be offered later), as are the network and broadcast addresses. Refused with 422 on cidr or name when they are not valid, private_network_name_taken, private_network_overlap, private_network_limit at the limit, private_network_host_unsupported when no active host in the location reports that it applies private networks, and server_not_active when the server is not an active VPS.

  • Permission: servers.manage
  • API token: not available, use a signed-in session
  • Step-up: needs a recent password check

Request body (JSON):

{
  "cidr": "string",
  "name": "string",
  "server_id": "00000000-0000-0000-0000-000000000000"
}
ResponseMeaning
201The private network
defaultError

DELETE /api/v1/orgs/{org}/private-networks/{private_network}

Delete a private network

Deletes a private network that no server is on. It happens at once (no host is involved). A network with a server attached, being attached or being taken off is refused with private_network_in_use: a delete never takes a network away from a running server, so detach the servers first (each detach is confirmed). The bridge number of a deleted network is never reused.

  • Permission: servers.manage
  • API token: not available, use a signed-in session
  • Step-up: needs a recent password check
ResponseMeaning
204Deleted
defaultError

GET /api/v1/orgs/{org}/private-networks/{private_network}

One private network

One private network of the organisation, as the list shows it. A deleted network is a 404.

  • Permission: servers.read
  • API token scope: servers:read
ResponseMeaning
200The private network
defaultError

Example:

curl -X GET "$VEXELIA_URL/api/v1/orgs/$org/private-networks/$private_network" \
  -H "Authorization: Bearer $VEXELIA_TOKEN"

POST /api/v1/orgs/{org}/private-networks/{private_network}/attach

Attach a server to a private network

Gives the server server_id a second network interface on the network, with a private address from its range: address if given (it must be inside the range and not the network, first-host or broadcast address), else the lowest free one. The server's host plugs the interface into the network's own bridge and locks it to its MAC address and its private address: it can send nothing else, cannot run DHCP, and sees only IPv4 and ARP of its own network. The guest is not configured; the customer sets the address inside it (a static address on the new interface). The attach is one operation: the member reads attaching until the host answers, attached after, and is gone again with error_code if the host could not. The server's firewall policy filters its public interface only and does not filter private network traffic (firewall_applies is false): when the server has a firewall policy attached, the body must say firewall_ack true to show that this is understood (private_network_firewall_ack_required otherwise). Every server of a network runs on the same host: private_network_other_node when the server is on another host from the network's other servers, private_network_other_group when it is in another location. A server may be on two networks and a network may hold 32 servers. Refused also with private_network_already_member, private_network_address_taken, private_network_no_room, private_network_server_full, private_network_full, server_not_active, renewal_charging while its renewal is being paid, private_network_host_unsupported when the host has not reported that it applies private networks, and server_busy while another action runs on the server.

  • Permission: servers.manage
  • API token: not available, use a signed-in session
  • Step-up: needs a recent password check

Request body (JSON):

{
  "address": "string",
  "firewall_ack": false,
  "server_id": "00000000-0000-0000-0000-000000000000"
}
ResponseMeaning
202The attach was queued
defaultError

POST /api/v1/orgs/{org}/private-networks/{private_network}/detach

Take a server off a private network

Removes the server's interface from the network. Its traffic on the network stops as soon as the host has applied it, so the body must say confirm true (private_network_confirm_required otherwise); nothing else ever takes an interface off a running server. The detach is one operation: the member reads detaching until the host answers, then it is gone, or attached again with error_code if the host could not. The private address is free for another server afterwards. Refused with private_network_not_member when the server is not on the network, private_network_busy while its interface is changing, server_busy while another action runs on the server and private_network_host_unsupported when the host has not reported that it applies private networks.

  • Permission: servers.manage
  • API token: not available, use a signed-in session
  • Step-up: needs a recent password check

Request body (JSON):

{
  "confirm": false,
  "server_id": "00000000-0000-0000-0000-000000000000"
}
ResponseMeaning
202The detach was queued
defaultError

proformas

GET /api/v1/orgs/{org}/proformas/{proforma}

A renewal that is due

Returns a renewal that is due: the period, totals, credit held and card amount, charge attempts, next retry and suspension date, and whether it can still be paid (payable). Returns not_found if it belongs to another organisation.

  • Permission: billing.read
  • API token: not available, use a signed-in session
ResponseMeaning
200Renewal
defaultError

POST /api/v1/orgs/{org}/proformas/{proforma}/pay

Pay or confirm a renewal

Pays or confirms a renewal with credit and a saved card or a new card, and returns the status, a client secret if the card needs confirmation, and the credit and card amounts. Returns proforma_not_payable if it is no longer open and payment_unavailable if payments are not available.

  • Permission: billing.manage
  • API token: not available, use a signed-in session

Request body (JSON):

{
  "new_card": false,
  "payment_method": "00000000-0000-0000-0000-000000000000"
}
ResponseMeaning
200Where the payment stands
defaultError

promotions

POST /api/v1/orgs/{org}/promotions/validate

Check promotion codes against a plan before ordering

Prices a plan with the codes the customer typed, using the same rules and arithmetic as the order, and records nothing: no use of a code is taken. Answers the list price, the discount, the price after it, the tax estimated on that price and the total. A code that does not hold is validation_failed on promo_code with a message that says why. Rate limited like a write, so codes cannot be guessed quickly.

  • Permission: servers.read
  • API token: not available, use a signed-in session

Request body (JSON):

{
  "price_id": "00000000-0000-0000-0000-000000000000",
  "promo_codes": [
    null
  ]
}
ResponseMeaning
200The priced plan
defaultError

public

GET /api/v1/public/catalog

What is on sale: regions, plans, prices in force and images

No sign-in. Only what a customer could buy today, with the prices in force now, so a pricing page matches checkout. Hidden and retired plans, future price versions, stock figures, internal ids and payment details are never included. Answers carry Cache-Control public, max-age=300, and Access-Control-Allow-Origin only for the configured site origins (VEXELIA_SITE_ORIGINS; default vexelia.com and www.vexelia.com). Has its own rate limit per address.

  • Access: public, no authentication
ResponseMeaning
200The public catalog
defaultError

GET /api/v1/public/status

The platform's current status, by component, with open incidents

No sign-in. A component's status comes from the open incidents that affect it, never from a setting, so it cannot read operational while one is open. Answers carry Cache-Control public, max-age=30 and CORS headers for the configured site origins.

  • Access: public, no authentication
ResponseMeaning
200Overall and per-component status
defaultError

GET /api/v1/public/status/incidents

Past and current incidents with updates and post-mortems

No sign-in. Newest first. Incidents are never deleted, so this is the platform's whole incident history.

  • Access: public, no authentication
ParameterInNotes
limitquery
beforequeryThe next_before value of the previous page
ResponseMeaning
200One page of incidents
defaultError

quotes

GET /api/v1/orgs/{org}/quotes

List the quotes sent to this account

Quotes staff issued to this organisation, newest first, with their state (an open quote past its date is expired). Quotes of other organisations are never shown.

  • Permission: billing.read
  • API token: not available, use a signed-in session
ResponseMeaning
200The quotes
defaultError

GET /api/v1/orgs/{org}/quotes/{quote}

Get a quote

One quote with its line, discount, validity date and an estimate of tax and total.

  • Permission: billing.read
  • API token: not available, use a signed-in session
ResponseMeaning
200The quote
defaultError

POST /api/v1/orgs/{org}/quotes/{quote}/accept

Accept a quote and start its order

Turns an open quote into the normal order path: the customer gives the region, image, hostname and keys, and an order is made at the quote's price (the discount staff gave is applied before tax like a promotion code), with the same limits, capacity checks and payment as any order. Returns 201 with the order and a client_secret when a card payment is needed. The quote becomes accepted when that order is paid. Repeating the call returns the order already made while it awaits payment, or the paid one. Refused with validation_failed on quote when the quote has expired, was declined or withdrawn, or the plan's price changed since it was written.

  • Permission: servers.manage
  • API token: not available, use a signed-in session

Request body (JSON):

{
  "autopay": false,
  "hostname": "string",
  "image_id": "00000000-0000-0000-0000-000000000000",
  "region": "string",
  "ssh_key_ids": [
    null
  ],
  "user_data": "string"
}
ResponseMeaning
201The order
defaultError

POST /api/v1/orgs/{org}/quotes/{quote}/decline

Decline a quote

Turns an open quote down. Refused while an order made from it is waiting for payment. Audited.

  • Permission: billing.manage
  • API token: not available, use a signed-in session
ResponseMeaning
200The quote
defaultError

roles

GET /api/v1/orgs/{org}/roles

List roles and the permission catalog

Lists the five built-in roles (fixed), the organisation's own roles with how many members hold each, and every permission a role can hold with a one-line meaning.

  • Permission: org.members.read
  • API token: not available, use a signed-in session
ResponseMeaning
200Roles
defaultError

POST /api/v1/orgs/{org}/roles

Create a role

Defines a role of the organisation's own from permissions in the catalog. You can only include permissions you hold yourself (403 role_escalation), a name may not be a built-in role's or already used (422), and an organisation has at most 20 (409 role_limit). A personal organisation has none (409 personal_org). Nobody holds the role until a member is given it.

  • Permission: org.members.manage
  • API token: not available, use a signed-in session

Request body (JSON):

{
  "description": "string",
  "name": "string",
  "permissions": [
    null
  ]
}
ResponseMeaning
201Created
defaultError

DELETE /api/v1/orgs/{org}/roles/{role}

Delete a role

Deletes a role nobody holds. A role in use is refused (409 role_in_use): move its members to another role first. Needs a recent step-up and a role whose permissions you hold in full.

  • Permission: org.members.manage
  • API token: not available, use a signed-in session
  • Step-up: needs a recent password check
ResponseMeaning
204Done
defaultError

PATCH /api/v1/orgs/{org}/roles/{role}

Change a role

Replaces the name, description and permissions of a role. Everyone who holds it changes with it. Needs a recent step-up. You can only change a role whose permissions you hold in full, and only to permissions you hold (403 role_escalation).

  • Permission: org.members.manage
  • API token: not available, use a signed-in session
  • Step-up: needs a recent password check

Request body (JSON):

{
  "description": "string",
  "name": "string",
  "permissions": [
    null
  ]
}
ResponseMeaning
200Changed
defaultError

servers

GET /api/v1/orgs/{org}/servers

List servers

Newest first, with the server id as tiebreak. Repeat the label query parameter to keep only servers that carry every label given (case does not matter). Without limit and cursor every matching server is returned; with them the list is cursor paged (limit 1 to 200) and next_cursor is the cursor for the next page, null on the last. An invalid cursor or limit is 422.

  • Permission: servers.read
  • API token scope: servers:read
ParameterInNotes
labelquery
limitqueryPage size, 1 to 200 (a larger value is lowered to 200). Without limit and cursor the whole list is returned, except where the operation says otherwise.
cursorqueryThe next_cursor of the previous page. Opaque; valid only for the list that issued it.
ResponseMeaning
200Servers
defaultError

Example:

curl -X GET "$VEXELIA_URL/api/v1/orgs/$org/servers" \
  -H "Authorization: Bearer $VEXELIA_TOKEN"

GET /api/v1/orgs/{org}/servers/{server}

Get a server

Returns one server: its plan, image, addresses, power state, lifecycle and any action still running. Poll it, or the operation it names, after a power, rebuild or firewall change.

  • Permission: servers.read
  • API token scope: servers:read
ResponseMeaning
200Server
defaultError

Example:

curl -X GET "$VEXELIA_URL/api/v1/orgs/$org/servers/$server" \
  -H "Authorization: Bearer $VEXELIA_TOKEN"

GET /api/v1/orgs/{org}/servers/{server}/apps

The apps installed on a server and the apps it can install

The server's apps, newest first (installed, being installed, failed, and removed by a rebuild), each with its state, where it answers, where its credentials are, and the choices made; and the apps that may be installed on an existing server. A queued or running install the server has said nothing about for 45 minutes shows as unconfirmed. Answers not_found when the marketplace is switched off. Closed to API tokens.

  • Permission: servers.read
  • API token: not available, use a signed-in session
ResponseMeaning
200Apps
defaultError

POST /api/v1/orgs/{org}/servers/{server}/apps

Install an app on an existing server

Queues the install of one signed first-party app on a server that is already in use. Run the plan first: it shows every check and what will change. The server restarts once (confirm_restart must be true, app_restart_confirm_required otherwise); the host verifies the app's signature with its own key, generates every password itself and a Vexelia installer runs as root inside the server, refusing to run over files, packages, users or ports it did not create. Refused with 409 when the server is not running (app_server_not_running), is not Debian or Ubuntu (app_unsupported_os), the plan is too small (app_plan_too_small), the host cannot install apps (app_host_unsupported), the app is already installed (app_already_installed), another install runs (app_install_busy), a port is taken by another app (app_port_conflict), or a limit is reached (app_limit, app_too_many_failures, app_rate_limited); with 422 on inputs (including a password, which is never accepted). A repeat with the same Idempotency-Key returns the same install. Returns 202; progress is the app's state. Needs a verified email and step-up. Closed to API tokens.

  • Permission: servers.manage
  • API token: not available, use a signed-in session
  • Step-up: needs a recent password check
ParameterInNotes
Idempotency-Keyheader

Request body (JSON):

{
  "confirm_restart": false,
  "inputs": {},
  "recipe_id": "string"
}
ResponseMeaning
202Install queued
defaultError

POST /api/v1/orgs/{org}/servers/{server}/apps/plan

Check an app install on a server and show what it would change

A dry run: validates the choices exactly as the host will, checks the server (running, supported system and processor, enough disk and memory, a host that can install, no other install running, no port clash with the apps already there) and lists what would change, including the restart and whether a snapshot first is advised. Nothing is written and no host is asked. Every finding is in the answer, with ok false when any check failed. Answers 422 on recipe_id for an app that is not offered for existing servers.

  • Permission: servers.manage
  • API token: not available, use a signed-in session

Request body (JSON):

{
  "inputs": {},
  "recipe_id": "string"
}
ResponseMeaning
200The plan
defaultError

GET /api/v1/orgs/{org}/servers/{server}/apps/{app}

One app on a server and its install progress

The app, its state and where it answers. The state comes from the customer's own server, so treat it as a hint. An app of another server or organisation is not found.

  • Permission: servers.read
  • API token: not available, use a signed-in session
ResponseMeaning
200App
defaultError

POST /api/v1/orgs/{org}/servers/{server}/apps/{app}/abandon

Give up on an install that is not finishing

Ends a queued or running install: it becomes failed with the reason user_gave_up, leaves the live set (the app can be installed again) and releases the server (a resize or a restore is no longer held back). Nothing is undone on the server: what the installer already did stays. Refused with 409 app_install_running while the host reports the install running (its operation running, or progress in the last ten minutes) and 409 app_not_abandonable when it already ended. Installs that no host picks up are ended automatically after 2 hours, and ones silent for hours after the stale window. Needs step-up. Closed to API tokens.

  • Permission: servers.manage
  • API token: not available, use a signed-in session
  • Step-up: needs a recent password check
ResponseMeaning
200The ended install
defaultError

POST /api/v1/orgs/{org}/servers/{server}/apps/{app}/changes

Uninstall or update an app on an existing server

Queues the uninstall or the update of one installed app. Run the plan first. The server restarts once (confirm_restart must be true, app_restart_confirm_required otherwise); the host verifies the app's signature with its own key and a Vexelia script runs as root inside the server, removing only what the installer created or updating the app in place. Choosing to remove the app's data needs confirm_data_removal: true (app_data_removal_confirm_required otherwise) and the plan's data_digest echoed as data_digest (app_data_removal_changed when what would be deleted is no longer what the plan listed); keep_data defaults to true. The change carries no password or other secret. Refused with 409 when the app is not ready (app_change_not_ready), the recipe has no such step or the installed version is too old to update (app_change_unsupported), it is already at the newest version (app_up_to_date), another install or change holds the server (app_change_busy), the server is not running (app_server_not_running), the host cannot change apps (app_host_unsupported), or a limit is reached (app_too_many_failures, app_rate_limited). A repeat with the same Idempotency-Key returns the same change. Returns 202; progress is the change's state, and an uninstall that succeeds moves the app to removed, an update records the new version. Needs a verified email and step-up. Closed to API tokens.

  • Permission: servers.manage
  • API token: not available, use a signed-in session
  • Step-up: needs a recent password check
ParameterInNotes
Idempotency-Keyheader

Request body (JSON):

{
  "action": "uninstall",
  "confirm_data_removal": false,
  "confirm_restart": false,
  "data_digest": "string",
  "keep_data": false
}
ResponseMeaning
202Change queued
defaultError

GET /api/v1/orgs/{org}/servers/{server}/apps/{app}/changes/{change}

One uninstall or update of an app and its progress

The change, its state and the step it stopped at. The state comes from the customer's own server, so treat it as a hint. A change of another app, server or organisation is not found.

  • Permission: servers.read
  • API token: not available, use a signed-in session
ResponseMeaning
200Change
defaultError

POST /api/v1/orgs/{org}/servers/{server}/apps/{app}/changes/{change}/abandon

Give up on an uninstall or update that is not finishing

Ends a queued or running change: it becomes failed with the reason user_gave_up and releases the server (a resize or a restore is no longer held back). The app stays installed as the record had it; nothing is undone on the server, so what the script already did stays. Refused with 409 app_change_running while the host reports the change running (its operation running, or progress in the last ten minutes) and 409 app_change_not_abandonable when it already ended. Changes that no host picks up are ended automatically after 2 hours, and ones silent for hours after the stale window. Needs step-up. Closed to API tokens.

  • Permission: servers.manage
  • API token: not available, use a signed-in session
  • Step-up: needs a recent password check
ResponseMeaning
200The ended change
defaultError

POST /api/v1/orgs/{org}/servers/{server}/apps/{app}/credentials/reveal

Show an app's generated credentials once

Returns the passwords the host generated for this install and marks them shown. The first call gets them (Cache-Control: no-store); every later call is 409 credentials_viewed, and a call before the host delivered them is 409 app_no_credentials. Recorded in Activity by key count, never by value. Needs step-up. Closed to API tokens.

  • Permission: servers.manage
  • API token: not available, use a signed-in session
  • Step-up: needs a recent password check
ResponseMeaning
200The credentials
defaultError

POST /api/v1/orgs/{org}/servers/{server}/apps/{app}/plan

Check an uninstall or update of an app and show what it would change

A dry run for uninstalling or updating an installed app: checks the app (finished installing, the recipe still offered with the step you asked for, and for an update a newer version the recipe can update this install to), the server (active, running, not in rescue or on an ISO), the host (it reports it can change apps), that no install or other change holds the server, and the abuse limits. It says what goes whatever you choose (removes), the data that is kept or removed with your choice (data_choice, keep_data, data_kept, data_removed; the choice defaults to keeping the data), what an update changes (notes), what happens to the server (the restart and the root script; a failure part-way leaves what it did, and a snapshot taken first is the only way back) and whether a snapshot is advised. Nothing is written and no host is asked. An app of another server or organisation is not found.

  • Permission: servers.manage
  • API token: not available, use a signed-in session

Request body (JSON):

{
  "action": "uninstall",
  "keep_data": false
}
ResponseMeaning
200The plan
defaultError

GET /api/v1/orgs/{org}/servers/{server}/automation

A server's user-data, startup scripts and cloud-init switch

What the server is given on its first boot after a rebuild: the cloud-init user-data, the named startup scripts in order, the cloud-init switch, and the variables the text may use. The text may hold the caller's own secrets, so it needs servers.manage, is never cached, and never appears in the audit log. It runs as root on the first boot only; changing it does not touch a running server, it is used at the next rebuild.

  • Permission: servers.manage
  • API token: not available, use a signed-in session
ResponseMeaning
200The server's automation
defaultError

PUT /api/v1/orgs/{org}/servers/{server}/automation

Set a server's user-data or cloud-init switch

Sets the user-data (a #cloud-config document or a #! script, up to 64 KiB; #include is refused; a cloud-config that is not valid YAML is accepted with a warning) and/or the cloud-init switch. Variables are written ${vexelia.server.id}, ${vexelia.server.hostname}, ${vexelia.net.ipv4}, ${vexelia.net.ipv6_prefix} and ${vexelia.org.KEY}; any other ${vexelia.x} is refused naming it, and other ${...} (a shell's own) is left alone. Substitution happens once, so a value that contains ${vexelia.x} is not expanded again. The user-data and scripts together are at most 192 KiB.

  • Permission: servers.manage
  • API token: not available, use a signed-in session

Request body (JSON):

{
  "cloud_init_enabled": false,
  "user_data": "string"
}
ResponseMeaning
200The server's automation after the change
defaultError

POST /api/v1/orgs/{org}/servers/{server}/automation/scripts

Add a startup script

Adds a named script that starts with a

  • Permission: servers.manage
  • API token: not available, use a signed-in session

Request body (JSON):

{
  "content": "string",
  "name": "string",
  "position": 0
}
ResponseMeaning
200The server's automation after the change
defaultError

DELETE /api/v1/orgs/{org}/servers/{server}/automation/scripts/{script}

Delete a startup script

Deletes a startup script from the server. Returns not_found if the script does not exist on this server.

  • Permission: servers.manage
  • API token: not available, use a signed-in session
ResponseMeaning
200The server's automation after the change
defaultError

PUT /api/v1/orgs/{org}/servers/{server}/automation/scripts/{script}

Change a startup script

Replaces the script's name and text. Its position stays.

  • Permission: servers.manage
  • API token: not available, use a signed-in session

Request body (JSON):

{
  "content": "string",
  "name": "string"
}
ResponseMeaning
200The server's automation after the change
defaultError

POST /api/v1/orgs/{org}/servers/{server}/autopay

Turn automatic renewal on or off

Turns automatic renewal on or off for a server. Turning it on needs a saved card to charge (autopay_needs_card).

  • Permission: billing.manage
  • API token: not available, use a signed-in session

Request body (JSON):

{
  "autopay": false
}
ResponseMeaning
204Done
defaultError

GET /api/v1/orgs/{org}/servers/{server}/backups

List a server's backups

The server's backups, newest first, and its backup requests that are waiting or were refused. A backup is ready once its manifest has been read back and checked; incomplete means the node stopped part way; unusable means a stored object failed its check and it is never restored from. A stopped server's last ready backup stands (power_state says whether the server is stopped). consistency is application when the guest agent froze the disk, crash otherwise.

  • Permission: servers.read
  • API token scope: servers:read
ResponseMeaning
200Backups
defaultError

Example:

curl -X GET "$VEXELIA_URL/api/v1/orgs/$org/servers/$server/backups" \
  -H "Authorization: Bearer $VEXELIA_TOKEN"

POST /api/v1/orgs/{org}/servers/{server}/backups

Back a server up now

Asks for a backup of the primary disk, or of the disks named, now. The worker starts it as soon as the server is free; it is incremental unless full is true or the server's disk was replaced, and a stopped server's backup is a full read of its disk. Limited to 2 requests per server in any 10 minutes (backup_cooldown, with retry_at), refused with backup_queued when that disk already has one waiting or running, and with server_not_active unless the server is active.

  • Permission: servers.manage
  • API token: not available, use a signed-in session

Request body (JSON):

{
  "disks": [
    null
  ],
  "full": false
}
ResponseMeaning
202Queued
defaultError

DELETE /api/v1/orgs/{org}/servers/{server}/backups/addon

Turn the Backups addon off at the end of the paid period

The addon keeps working until the end of what is paid for (ends_at) and the renewal that starts then no longer carries it. There is no credit for the rest of the period (decision 0008 applied to addons). Existing backups stay listed and restorable until retention removes them; no new manual or scheduled backup is made once the addon ends, and the schedule is switched off then. Repeating the call changes nothing; turning the addon on again before ends_at withdraws it at no charge. addon_not_enabled when the addon is not on; renewal_charging while the server's renewal is being paid.

  • Permission: servers.manage
  • API token: not available, use a signed-in session
  • Step-up: needs a recent password check
ResponseMeaning
200When the addon stops
defaultError

GET /api/v1/orgs/{org}/servers/{server}/backups/addon

Show a server's Backups addon and its price

Whether the Backups addon is on (state off, on, or ending: turned off, still working until ends_at), its price for one billing period at the price the next renewal uses (the greater of floor_minor_per_month times the months of the period and share_bp ten-thousandths of the plan's price for the period; both are staff settings), any open quote to pay, and the amount of a small charge deferred to the next renewal. Manual and scheduled backups need the addon (backup_addon_required without it); restoring and listing backups do not.

  • Permission: servers.read
  • API token scope: servers:read
ResponseMeaning
200The addon
defaultError

Example:

curl -X GET "$VEXELIA_URL/api/v1/orgs/$org/servers/$server/backups/addon" \
  -H "Authorization: Bearer $VEXELIA_TOKEN"

POST /api/v1/orgs/{org}/servers/{server}/backups/addon

Turn the Backups addon on

Prices the addon for the rest of the period the customer has paid for (remaining over total as an exact duration, rounded half up once; a renewal paid before its period began adds that whole period too), plus tax. The response carries it as charge, which stands for 30 minutes and is paid with POST .../backups/addon/charges/{charge}/pay (credit first, then card or 3-D Secure); the addon is on once the payment settles. When nothing is to be paid first (a zero charge, a charge below the card minimum that the billing policy defers to the renewal, a turn-off withdrawn before it took effect, or the addon already on) enabled is true. The renewal after the paid period bills the addon whole as a line of its own at the plan's price then (decision 0012). Refused with renewal_charging while the server's renewal is being paid, addon_charge_open while another quote is being paid, paid_period_ended when the paid period has ended, server_not_active unless the server is active. Answers 503 tax_unavailable while tax cannot be worked out yet for the buyer's location; nothing is created or charged, and trying again later succeeds once it is set up.

  • Permission: servers.manage
  • API token: not available, use a signed-in session
  • Step-up: needs a recent password check
ResponseMeaning
202A charge to pay first, or the addon already on
defaultError

POST /api/v1/orgs/{org}/servers/{server}/backups/addon/charges/{charge}/pay

Pay for turning the Backups addon on

Pays the addon's charge: wallet credit is held first, then one card intent covers the rest (the client secret is returned for Stripe.js), or the charge settles at once when credit covers it. Settlement issues an addon invoice, turns the addon on and voids the renewal that was issued without it. Repeating the call shows the same intent. quote_expired after 30 minutes (ask again), paid_period_ended if a renewal settled or the period ended since the quote, amount_below_minimum when the card part is below what the processor can charge. A payment that arrives after the quote can no longer be used is refunded to the card as a stray payment.

  • Permission: servers.manage
  • API token: not available, use a signed-in session
  • Step-up: needs a recent password check

Request body (JSON):

{
  "payment_method": "00000000-0000-0000-0000-000000000000"
}
ResponseMeaning
200Where the payment stands
defaultError

GET /api/v1/orgs/{org}/servers/{server}/backups/offsite

Show a server's off-site backup copy

Whether an off-site provider is connected (available), whether the weekly off-site copy is on for this server (decision 0019; on only while the Backups addon works), when it was turned on, when the last copy and the last monthly test restore succeeded, and the most recent runs (newest first, at most 12). A copy is an independent encrypted full image of the server's first disk kept with a second storage provider; a week with no succeeded run is a missed week. Listing needs no addon.

  • Permission: servers.read
  • API token: not available, use a signed-in session
ResponseMeaning
200The off-site copy
defaultError

PUT /api/v1/orgs/{org}/servers/{server}/backups/offsite

Turn a server's off-site backup copy on or off

Opts the server in (enabled true) or out (enabled false) of the weekly off-site copy. Turning it on needs an off-site provider to be connected (offsite_unavailable, 409, when none is) and the Backups addon (backup_addon_required without it); turning it off never needs either, and repeating either call changes nothing and is not audited again. Existing copies are not deleted by turning it off. The copy itself is made later by the platform's worker, not by this call. Audited when the state changes.

  • Permission: servers.manage
  • API token: not available, use a signed-in session

Request body (JSON):

{
  "enabled": false
}
ResponseMeaning
200The off-site copy after the change
defaultError

POST /api/v1/orgs/{org}/servers/{server}/backups/restore

Restore a backup over a server

Replaces the server's first disk with the backup chosen. The server in the path is the one restored onto; the backup may be one of another server of the same organisation (a backup of another organisation answers not_found). The server's current disk is kept in node trash for 72 hours, the server keeps its addresses, and the node checks the whole backup before it touches the disk (a damaged backup is refused with the server untouched). The server is stopped, its disk swapped, started, and must report running before the restore counts as done. Needs step-up and the server's hostname in confirm_hostname. Refused with server_not_active unless the server is active, restore_open when another restore onto it is waiting or running, and validation_failed when the backup is not of a first disk or the server's disk is smaller than the backup. To restore into a new server instead, order one with restore_backup_id (POST /orgs/{org}/orders).

  • Permission: servers.manage
  • API token: not available, use a signed-in session
  • Step-up: needs a recent password check

Request body (JSON):

{
  "backup_id": "00000000-0000-0000-0000-000000000000",
  "confirm_hostname": "string"
}
ResponseMeaning
202Queued
defaultError

GET /api/v1/orgs/{org}/servers/{server}/backups/restore-point

Find the backup to restore for a time

Restore to a point in time. Backups are taken by schedule or on request with nothing stored between them, so the closest point is the newest ready backup of the server's first disk taken at or before the time in at (RFC 3339; a time in the future is read as now). The answer names the backup and taken_at, the time its disk image was frozen, so the customer sees what a restore will really bring back. Answers not_found when no ready backup that early is kept. Use the backup's id with the restore route, or as restore_backup_id on an order. Finer points between backups cannot be restored.

  • Permission: servers.read
  • API token scope: servers:read
ParameterInNotes
atquery
ResponseMeaning
200The backup
defaultError

Example:

curl -X GET "$VEXELIA_URL/api/v1/orgs/$org/servers/$server/backups/restore-point" \
  -H "Authorization: Bearer $VEXELIA_TOKEN"

GET /api/v1/orgs/{org}/servers/{server}/backups/restores

List restores onto a server

The latest restores requested onto this server. refusal says why a restore failed or was refused (server_busy, server_not_ready, server_changed, backup_unusable, corrupt, store_unavailable, post_restore_check_failed). mode is overwrite for a restore over this server's disk, and new_server for the restore a new-server order owes (order_id names the order). A new-server restore is refused with server_changed when the customer has rebuilt, resized, restored or otherwise worked on the new server before it started: the backup is then not put over that work.

  • Permission: servers.read
  • API token scope: servers:read
ResponseMeaning
200Restores
defaultError

Example:

curl -X GET "$VEXELIA_URL/api/v1/orgs/$org/servers/$server/backups/restores" \
  -H "Authorization: Bearer $VEXELIA_TOKEN"

GET /api/v1/orgs/{org}/servers/{server}/backups/schedule

Show a server's backup schedule

Whether daily backups are on, the preferred start time and window in the chosen time zone, the allowed days (a bit mask, Monday 1 to Sunday 64), the extra disks included, the next three start times, and when the last run started or was missed. next_runs is empty while the schedule is off or paused; a suspended server's schedule pauses and resumes at the first window after it is active again. disks lists the disks that can be chosen.

  • Permission: servers.read
  • API token scope: servers:read
ResponseMeaning
200Schedule
defaultError

Example:

curl -X GET "$VEXELIA_URL/api/v1/orgs/$org/servers/$server/backups/schedule" \
  -H "Authorization: Bearer $VEXELIA_TOKEN"

PUT /api/v1/orgs/{org}/servers/{server}/backups/schedule

Change a server's backup schedule

Turns daily backups on or off and sets the preferred start, window and days. The start is best effort; if the worker cannot start it inside the window the run shows as missed. Extra disks must be attached now. Refused with server_not_active when turning it on for a server that is not active.

  • Permission: servers.manage
  • API token: not available, use a signed-in session

Request body (JSON):

{
  "allowed_days": 0,
  "enabled": false,
  "extra_disks": [
    null
  ],
  "tz": "string",
  "window_minutes": 0,
  "window_start_min": 0
}
ResponseMeaning
200Schedule
defaultError

GET /api/v1/orgs/{org}/servers/{server}/bandwidth

Traffic used this billing period and the quota

The traffic counted for the server in its current billing period, against the plan's quota. Periods are monthly and begin on the day of the subscription's paid_until (the 31st clamps to the last day of a short month). Counting is exact: the node reports monotonic byte counters with a sequence number, so a report sent twice counts once, a counter that restarted counts from zero, and a node that was offline and sends its backlog puts the bytes into the period they were used in. direction says what counts against the quota: in, out, or both added together. An unlimited plan has unlimited true and a null quota_bytes and percent. threshold is 0, 80 or 100, the share of the quota that has been reached. warned_80_at and warned_100_at are when each notice was queued, once per period. history lists the closed periods before this one, newest first. A server of another org is a 404.

  • Permission: servers.read
  • API token scope: servers:read
ResponseMeaning
200Bandwidth this period
defaultError

Example:

curl -X GET "$VEXELIA_URL/api/v1/orgs/$org/servers/$server/bandwidth" \
  -H "Authorization: Bearer $VEXELIA_TOKEN"

POST /api/v1/orgs/{org}/servers/{server}/cancel

Cancel a server at the end of its paid period

Stops renewals (the open renewal invoice is voided and automatic renewal is turned off) and sets the server to be destroyed at paid_until. No refund is given for the unused part. Repeating it is a no-op. Refused with cancel_unavailable once the paid period has ended.

  • Permission: servers.destroy
  • API token scope: servers:destroy
  • Step-up: needs a recent password check
ParameterInNotes
Idempotency-KeyheaderOptional with an API token: a new string (1 to 100 printable ASCII characters) per logical request. Repeating the request with the same key returns the first answer and does not repeat the action (response header Idempotent-Replay: true); the same key with a different request is 422 idempotency_key_reused. Kept 24 hours, per token and route.
ResponseMeaning
200Cancelling
defaultError

Example:

curl -X POST "$VEXELIA_URL/api/v1/orgs/$org/servers/$server/cancel" \
  -H "Authorization: Bearer $VEXELIA_TOKEN"

POST /api/v1/orgs/{org}/servers/{server}/cancel/undo

Undo a cancellation before the period ends

Puts the server back to normal. Automatic renewal stays off; the customer can turn it on again. Refused with not_cancelling when nothing is scheduled or the period has ended.

  • Permission: servers.destroy
  • API token scope: servers:destroy
ParameterInNotes
Idempotency-KeyheaderOptional with an API token: a new string (1 to 100 printable ASCII characters) per logical request. Repeating the request with the same key returns the first answer and does not repeat the action (response header Idempotent-Replay: true); the same key with a different request is 422 idempotency_key_reused. Kept 24 hours, per token and route.
ResponseMeaning
200No longer cancelling
defaultError

Example:

curl -X POST "$VEXELIA_URL/api/v1/orgs/$org/servers/$server/cancel/undo" \
  -H "Authorization: Bearer $VEXELIA_TOKEN"

POST /api/v1/orgs/{org}/servers/{server}/console

Get a single-use token to open a server's console

Returns a token valid for 60 seconds and one connection, and the edgegw WebSocket URL for the server's VNC or serial console. The browser opens `new WebSocket(ws_url, [protocol, token])`: the token travels in Sec-WebSocket-Protocol and never in a URL (a URL with a query string is refused). edgegw verifies the token, refuses a second use, and checks the person's permission, the server and the caps again when the token is used and for as long as the session lasts. Refused with console_server_stopped, console_server_suspended, console_rebuilding, console_node_unreachable or console_session_limit (one session per server and kind, four per person), and console_unavailable while the console is turned off. A closed WebSocket carries a close code in 4000-4999 naming the reason (token_expired, token_replayed, session_limit, vm_stopped, ended_by_staff, revoked, node_unreachable and others).

  • Permission: servers.manage
  • API token: not available, use a signed-in session
  • Session only: a person must be present

Request body (JSON):

{
  "kind": "vnc"
}
ResponseMeaning
201Token minted; it is not stored and is shown only here
defaultError

POST /api/v1/orgs/{org}/servers/{server}/destroy

Destroy a server

Destroys the server through the trash, where it can be restored for 72 hours. Needs the server's name in confirm_hostname. Refused with server_protected while the server is marked do-not-destroy. A token needs the servers:destroy scope, an IP allowlist or an expiry under 30 days.

  • Permission: servers.destroy
  • API token scope: servers:destroy
  • Step-up: needs a recent password check
ParameterInNotes
Idempotency-KeyheaderOptional with an API token: a new string (1 to 100 printable ASCII characters) per logical request. Repeating the request with the same key returns the first answer and does not repeat the action (response header Idempotent-Replay: true); the same key with a different request is 422 idempotency_key_reused. Kept 24 hours, per token and route.

Request body (JSON):

{
  "confirm_hostname": "string"
}
ResponseMeaning
202Terminating
defaultError

Example:

curl -X POST "$VEXELIA_URL/api/v1/orgs/$org/servers/$server/destroy" \
  -H "Authorization: Bearer $VEXELIA_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"confirm_hostname":"string"}'

POST /api/v1/orgs/{org}/servers/{server}/disk-charges/{charge}/pay

Pay for adding or growing an extra disk

Pays a disk charge: wallet credit is held first, then one card intent covers the rest (the client secret is returned for Stripe.js), or the charge settles at once when credit covers it. Settlement issues an invoice, reserves the disk (it is billed from then) and voids the renewal that was issued without it; the disk is then queued for the node. Repeating the call shows the same intent. quote_expired after 30 minutes (ask again), paid_period_ended if a renewal settled or the period ended since the quote, amount_below_minimum when the card part is below what the processor can charge.

  • Permission: servers.manage
  • API token: not available, use a signed-in session
  • Step-up: needs a recent password check

Request body (JSON):

{
  "payment_method": "00000000-0000-0000-0000-000000000000"
}
ResponseMeaning
200Where the payment stands
defaultError

GET /api/v1/orgs/{org}/servers/{server}/disks

A server's extra disks, with the plan's limits and price

The extra disks the server has now (being added, attached, growing or detaching) and what its plan allows. Prices are in minor units of the currency, per GiB per month; each disk renews at the plan's current per-GiB price under decision 0012 (a drop applies at once, a rise only if it took effect 30 days before the renewal). monthly_total_minor counts every disk that is paid for or attached (a disk being added or grown counts at its new size); it is added whole to each renewal.

  • Permission: servers.read
  • API token scope: servers:read
ResponseMeaning
200Disks and limits
defaultError

Example:

curl -X GET "$VEXELIA_URL/api/v1/orgs/$org/servers/$server/disks" \
  -H "Authorization: Bearer $VEXELIA_TOKEN"

POST /api/v1/orgs/{org}/servers/{server}/disks

Add an extra disk to a server

Prices a new blank disk of size_gib. The charge is the disk's price for the rest of the period the customer has paid for (remaining over total as an exact duration, rounded half up once; a renewal paid before its period began adds that whole period too), plus tax. The response carries it as charge, which stands for 30 minutes and is paid with POST .../disk-charges/{charge}/pay (credit first, then card or 3-D Secure); the disk is reserved, billed and queued for the node only after payment settles, and credited back to the wallet if the node cannot add it. When the charge is zero (a free price, or too little of the period left to round to a minor unit) the disk is queued at once and operation_id is set. The renewal after the paid period bills the disk whole at the plan's price (decision 0012). Refused with renewal_charging while the server's renewal is being paid, with disks_not_offered when the plan has none, disk_limit at the plan's count, node_full when the node has no room, disk_charge_open while another disk change is waiting to be paid or applied, paid_period_ended when the paid period has ended, server_busy while another action runs. A server defined before disks could be hot-plugged fails the operation with error_code no_hotplug_port; stop the server, add the disk, then start it. Answers 503 tax_unavailable while tax cannot be worked out yet for the buyer's location; nothing is created or charged, and trying again later succeeds once it is set up.

  • Permission: servers.manage
  • API token: not available, use a signed-in session
  • Step-up: needs a recent password check

Request body (JSON):

{
  "size_gib": 0
}
ResponseMeaning
202A charge to pay first
defaultError

POST /api/v1/orgs/{org}/servers/{server}/disks/{disk}/detach

Detach an extra disk

Takes the disk off the server; its file is kept in node trash for 72 hours. confirm_name must be the disk's name. The in-use check has limits. It asks the running guest's agent whether a filesystem on the disk is mounted. Use of a disk that is not mounted (raw database, LVM member, swap) is invisible to it, and a running server whose guest agent does not answer is refused unless force is true. A server that is not running can't be asked, so force is required for it too (409 disk_force_required). Forcing can lose data in the guest, and if /etc/fstab still lists the disk without the nofail option the server can drop into emergency mode at its next boot. A refusal by the node shows on the operation as error_code disk_in_use with an error_message. There is no credit for the rest of the paid period (decision 0008, VBD-1, applied to disks), so the period stands as paid and the next renewal no longer bills the disk. Refused with renewal_charging while the server's renewal is being paid.

  • Permission: servers.manage
  • API token: not available, use a signed-in session
  • Step-up: needs a recent password check

Request body (JSON):

{
  "confirm_name": "string",
  "force": false
}
ResponseMeaning
202Queued
defaultError

POST /api/v1/orgs/{org}/servers/{server}/disks/{disk}/grow

Grow an extra disk

Prices growing the disk to size_gib, online if the server runs. The charge is the extra GiB at the price the disk already has, for the rest of the paid period (as for adding a disk), and is paid with POST .../disk-charges/{charge}/pay before the node is asked; the larger size is billed from payment, and credited back to the wallet if the node cannot grow it. A zero charge queues the grow at once. The guest sees the new size at once and grows its filesystem itself. A disk never shrinks, so size_gib must be larger than the disk is now and within the plan. Answers 503 tax_unavailable while tax cannot be worked out yet for the buyer's location; nothing is created or charged.

  • Permission: servers.manage
  • API token: not available, use a signed-in session
  • Step-up: needs a recent password check

Request body (JSON):

{
  "size_gib": 0
}
ResponseMeaning
202A charge to pay first
defaultError

GET /api/v1/orgs/{org}/servers/{server}/extension-cards

Extension cards for a server's page

The cards of every enabled customer manifest for the slot `customer.server.detail.aside`, resolved for this server of the caller's own organisation. A server of another organisation, or one that does not exist, is a 404 whatever manifests exist. Customer cards are stricter than staff cards: at most 2 cards of 6 blocks, short text with no typed web address, email address or markup, rows only from the platform's data source, and links only to the server or to support. The slot is empty unless the flag `ext_slots_customer_enabled` is on for the organisation (off by default). Read-only, so it also works in an impersonation session. Every string is plain text.

  • Permission: servers.read
  • API token: not available, use a signed-in session
ResponseMeaning
200The cards
defaultError

DELETE /api/v1/orgs/{org}/servers/{server}/firewall

Detach the firewall policy from a server

Queues an apply_firewall operation that removes the customer firewall. Detaching when nothing is attached is a no-op. A terminated server can be detached too; nothing is sent to a node until it is restored. Detaching confirms a test that was running.

  • Permission: servers.manage
  • API token scope: firewall:write
ParameterInNotes
Idempotency-KeyheaderOptional with an API token: a new string (1 to 100 printable ASCII characters) per logical request. Repeating the request with the same key returns the first answer and does not repeat the action (response header Idempotent-Replay: true); the same key with a different request is 422 idempotency_key_reused. Kept 24 hours, per token and route.
ResponseMeaning
202Queued
defaultError

Example:

curl -X DELETE "$VEXELIA_URL/api/v1/orgs/$org/servers/$server/firewall" \
  -H "Authorization: Bearer $VEXELIA_TOKEN"

GET /api/v1/orgs/{org}/servers/{server}/firewall

Which firewall policy a server uses and whether it is applied

Returns the firewall attached to the server, how far the node is in applying it (apply_state), any test-mode countdown, and staff_reset when support has reset it to default-allow.

  • Permission: servers.read
  • API token scope: firewall:read
ResponseMeaning
200Attachment
defaultError

Example:

curl -X GET "$VEXELIA_URL/api/v1/orgs/$org/servers/$server/firewall" \
  -H "Authorization: Bearer $VEXELIA_TOKEN"

PUT /api/v1/orgs/{org}/servers/{server}/firewall

Attach a firewall policy to a server

One policy per server; attaching another replaces it. Queues an apply_firewall operation (follow operation_id at the operations route). A server that is busy with another action shows pending and is applied as soon as it is free. The policy must belong to the same organisation, else 404. With test_mode true the policy applies for two minutes and the node puts the previous firewall back by itself unless the change is kept (POST .../firewall/keep); the answer carries test_mode.expires_at. Refused with 409 firewall_lockout when the policy would newly block the caller's own SSH (the sentence is in fields.lockout) unless acknowledge_lockout is true, in test mode too. A policy filters the server's public interface only; when the server is on a private network the body must say private_network_ack true, else 409 private_network_firewall_ack_required, and every answer carries private_networks_unfiltered true.

  • Permission: servers.manage
  • API token scope: firewall:write
ParameterInNotes
Idempotency-KeyheaderOptional with an API token: a new string (1 to 100 printable ASCII characters) per logical request. Repeating the request with the same key returns the first answer and does not repeat the action (response header Idempotent-Replay: true); the same key with a different request is 422 idempotency_key_reused. Kept 24 hours, per token and route.

Request body (JSON):

{
  "acknowledge_lockout": false,
  "firewall_id": "00000000-0000-0000-0000-000000000000",
  "private_network_ack": false,
  "test_mode": false
}
ResponseMeaning
202Queued
defaultError

Example:

curl -X PUT "$VEXELIA_URL/api/v1/orgs/$org/servers/$server/firewall" \
  -H "Authorization: Bearer $VEXELIA_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"acknowledge_lockout":false,"firewall_id":"00000000-0000-0000-0000-000000000000","private_network_ack":false,"test_mode":false}'

POST /api/v1/orgs/{org}/servers/{server}/firewall/keep

Keep the firewall applied in test mode

Tells the node to cancel its revert timer, so the firewall applied in test mode stays. The test ends in the database when the node confirms. 409 firewall_no_test when the server is not in test mode, firewall_test_over when the two minutes are up (the previous firewall is already back), server_busy while the test firewall has not reached the node yet.

  • Permission: servers.manage
  • API token scope: firewall:write
ParameterInNotes
Idempotency-KeyheaderOptional with an API token: a new string (1 to 100 printable ASCII characters) per logical request. Repeating the request with the same key returns the first answer and does not repeat the action (response header Idempotent-Replay: true); the same key with a different request is 422 idempotency_key_reused. Kept 24 hours, per token and route.
ResponseMeaning
202Queued
defaultError

Example:

curl -X POST "$VEXELIA_URL/api/v1/orgs/$org/servers/$server/firewall/keep" \
  -H "Authorization: Bearer $VEXELIA_TOKEN"

POST /api/v1/orgs/{org}/servers/{server}/firewall/rollback

End test mode now and put the previous firewall back

Points the server at the firewall it had before the test and sends it as an ordinary change. 409 firewall_no_test when the server is not in test mode.

  • Permission: servers.manage
  • API token scope: firewall:write
ParameterInNotes
Idempotency-KeyheaderOptional with an API token: a new string (1 to 100 printable ASCII characters) per logical request. Repeating the request with the same key returns the first answer and does not repeat the action (response header Idempotent-Replay: true); the same key with a different request is 422 idempotency_key_reused. Kept 24 hours, per token and route.
ResponseMeaning
202Queued
defaultError

Example:

curl -X POST "$VEXELIA_URL/api/v1/orgs/$org/servers/$server/firewall/rollback" \
  -H "Authorization: Bearer $VEXELIA_TOKEN"

POST /api/v1/orgs/{org}/servers/{server}/firmware

Change what a stopped server boots

Switches a server between UEFI and BIOS, or turns Secure Boot on or off. The server must be stopped, refused with server_not_stopped otherwise, and with server_busy while another action is running. The node updates the server's boot setup and leaves it stopped; the change applies when the server next starts. Omitted fields stay as they are, and turning UEFI off turns Secure Boot off. BIOS is refused on aarch64 servers, which always boot EFI, and each choice is refused when the server's operating system can't boot it (see boot_options on the server). The UEFI variable store, with the boot entry, is kept unless the change makes it unusable, which is switching Secure Boot or leaving UEFI.

  • Permission: servers.manage
  • API token: not available, use a signed-in session
  • Step-up: needs a recent password check

Request body (JSON):

{
  "firmware": "uefi",
  "secure_boot": false
}
ResponseMeaning
202Queued; applies on the next start
defaultError

POST /api/v1/orgs/{org}/servers/{server}/hostname

Change a server's hostname

Writes the new hostname into a one-shot seed without restarting the server, so it applies at the server's next boot. Once the node has written it, the server shows it as pending_hostname; it becomes the hostname when the server next starts, restarts or has its password reset. A later change before then replaces it, and a password reset carries it.

  • Permission: servers.manage
  • API token: not available, use a signed-in session
  • Step-up: needs a recent password check

Request body (JSON):

{
  "hostname": "string"
}
ResponseMeaning
202Queued; applies on next reboot
defaultError

PUT /api/v1/orgs/{org}/servers/{server}/labels

Replace a server's labels

Replaces the whole set. Up to 20 labels of 1 to 40 characters; blanks are dropped and a label repeated in any case is kept once. Labels are the customer's own and are not shown to other organisations. The server list filters by them.

  • Permission: servers.manage
  • API token: not available, use a signed-in session

Request body (JSON):

{
  "labels": [
    null
  ]
}
ResponseMeaning
200The labels now
defaultError

GET /api/v1/orgs/{org}/servers/{server}/media

Show the ISO mounted on a server

What the node last confirmed. iso is null when nothing is mounted; boot is the boot order (cdrom first or disk first); server_arch is the CPU architecture of the node group the server runs on, which a mounted ISO must match.

  • Permission: servers.read
  • API token scope: servers:read
ResponseMeaning
200Mounted media
defaultError

Example:

curl -X GET "$VEXELIA_URL/api/v1/orgs/$org/servers/$server/media" \
  -H "Authorization: Bearer $VEXELIA_TOKEN"

POST /api/v1/orgs/{org}/servers/{server}/media

Mount an ISO on a server and set its boot order

Mounts a ready ISO from the organisation's library or the system library on the server's CD-ROM, replacing any mounted one, with the boot order given. Another organisation's ISO is refused as if it did not exist. An ISO built for another CPU architecture than the server's is refused with a field error on iso_id. It applies at the server's next boot; the server is not restarted. Refused with server_not_active unless the server is active, and server_busy while another action runs.

  • Permission: servers.manage
  • API token: not available, use a signed-in session

Request body (JSON):

{
  "boot": "cdrom",
  "iso_id": "00000000-0000-0000-0000-000000000000"
}
ResponseMeaning
202Queued; applies at the next boot
defaultError

POST /api/v1/orgs/{org}/servers/{server}/media/unmount

Unmount the ISO from a server

Takes the ISO off the server and puts the disk first again. A running server has the disc ejected now; if QEMU reports the tray locked by the guest the operation fails with error code media_locked and changes nothing, unless force is true. That refusal is only proved against fakes. On the real QEMU of the nested aarch64 lab a guest-locked tray did not refuse a plain unmount, so do not rely on media_locked appearing. With nothing mounted it changes nothing.

  • Permission: servers.manage
  • API token: not available, use a signed-in session

Request body (JSON):

{
  "force": false
}
ResponseMeaning
202Queued
defaultError

GET /api/v1/orgs/{org}/servers/{server}/network

A server's addresses, port speed and traffic

The server's IPv4 addresses (the first is primary) and IPv6 /64, each with the gateway the guest routes through, the plan's port speed, and the traffic seen in the last 30 days. Traffic is an estimate from the one-minute usage samples, not the billing meter: minutes with no sample count for nothing, and covered_minutes says how many minutes had one. in_bytes and out_bytes are null when there are no samples. assigning is true while a server being set up has no address yet. Each address carries its reverse DNS: the record in force (rdns), the newest request that is pending or was declined (rdns_request) and whether the record is forward-confirmed for mail (mail_eligible, null when there is no record; rdns_problem says what failed on the last check). The IPv6 range lists the addresses the guest uses and whether IPv6 is on; resolvers and network_apply carry the resolver choice and whether it has reached the node.

  • Permission: servers.read
  • API token scope: servers:read
ResponseMeaning
200The network tab
defaultError

Example:

curl -X GET "$VEXELIA_URL/api/v1/orgs/$org/servers/$server/network" \
  -H "Authorization: Bearer $VEXELIA_TOKEN"

PUT /api/v1/orgs/{org}/servers/{server}/network

Choose a server's IPv6 addresses and resolvers

Saves the whole choice: IPv6 on or off, the addresses inside the server's /64 it may use (at most 16; empty means the whole range with its first address primary), the one the guest configures, and the resolvers it is told (at most 4; empty means the platform's default). The node replaces the server's anti-spoofing allowlist with exactly these addresses and writes a new network seed. The server keeps running and takes the seed at its next restart, so the answer says applies: next_restart and never that it is applied; the network tab then shows network_apply as pending, applied or failed. A refused choice keeps the stored one. Refused with server_busy while another action is running.

  • Permission: servers.manage
  • API token: not available, use a signed-in session

Request body (JSON):

{
  "ipv6_addresses": [
    null
  ],
  "ipv6_enabled": false,
  "primary_ipv6": "string",
  "resolvers": [
    null
  ]
}
ResponseMeaning
202Queued; applies on the server's next restart
defaultError

POST /api/v1/orgs/{org}/servers/{server}/password

Reset a server's root password

Generates a new root password and returns it once, in this response. It is never stored or logged; the node gets only a hash, through a one-shot seed, and the server restarts to apply it. Refused with server_busy while another action is running.

  • Permission: servers.manage
  • API token: not available, use a signed-in session
  • Step-up: needs a recent password check

Request body (JSON):

{}
ResponseMeaning
202Queued; the password is shown only here
defaultError

POST /api/v1/orgs/{org}/servers/{server}/plan-change

Quote an upgrade or schedule a downgrade

A dearer plan is quoted as an upgrade (202): the response carries the quote, which stands for the policy's quote_minutes and is paid with POST plan-change/{change}/pay (credit first, then card or 3-D Secure). The node is asked to resize only after payment settles, and the server restarts once. A cheaper plan is scheduled for the next renewal (200) and gives no credit. Asking again for the same upgrade while its quote stands returns the same quote; another plan replaces the quote. Refused with plan_no_room when the host has no room for the larger plan (before any payment), plan_disk_shrink for a smaller disk, plan_change_open while a paid upgrade is being applied, plan_too_late when too little of the period is left to charge for, paid_period_ended after the paid period, downgrades_disabled when staff switched scheduling off, server_not_active for a server that is not running or is set to cancel, plan_other_class when the plan has another CPU class (shared or dedicated vCPUs) than the server, and plan_other_arch when it runs on another processor type (x86 or Arm). A server never changes class or architecture in a resize: such plans are listed as unavailable with reason other_class or other_arch, and moving to one means ordering a new server on it and moving the data. Answers 503 tax_unavailable while tax cannot be worked out yet for the buyer's location; nothing is created or charged, and trying again later succeeds once it is set up.

  • Permission: servers.manage
  • API token: not available, use a signed-in session
  • Step-up: needs a recent password check

Request body (JSON):

{
  "plan_id": "00000000-0000-0000-0000-000000000000"
}
ResponseMeaning
200A downgrade scheduled for the next renewal
202An upgrade quoted for payment
defaultError

POST /api/v1/orgs/{org}/servers/{server}/plan-change/cancel-downgrade

Withdraw a scheduled downgrade

Withdraws a downgrade scheduled for the next renewal. A renewal already invoiced at the lower price is issued again at the current plan's price. canceled is false when there was none, including when the renewal at the lower price has already settled.

  • Permission: servers.manage
  • API token: not available, use a signed-in session
ResponseMeaning
200Whether one was withdrawn
defaultError

POST /api/v1/orgs/{org}/servers/{server}/plan-change/{change}/pay

Pay for an upgrade

Pays an upgrade quote: wallet credit is held first, then one card intent covers the rest (the client secret is returned for Stripe.js), or the upgrade settles at once when credit covers it. Settlement issues an upgrade invoice and moves the subscription to the new plan's price; the worker then asks the node to resize the server, and credits the whole amount back to the wallet (never the card) if the node cannot. Repeating the call shows the same intent. quote_expired after the quote's time (ask again), paid_period_ended if a renewal settled since, amount_below_minimum when the card part is below what the processor can charge.

  • Permission: servers.manage
  • API token: not available, use a signed-in session
  • Step-up: needs a recent password check

Request body (JSON):

{
  "payment_method": "00000000-0000-0000-0000-000000000000"
}
ResponseMeaning
200Where the payment stands
defaultError

GET /api/v1/orgs/{org}/servers/{server}/plan-options

The plans a server can move to, with the price of an upgrade

Every VPS plan on sale for the server's billing period, each classified against the plan the server is on. An upgrade (dearer, and no fewer CPUs or less memory) is priced for now: the share of the paid period that remains, applied to the whole-period price difference, rounded half up once, plus tax. The page shows a preview; asking for the change (POST plan-change) stores the quote for the policy's quote_minutes. A downgrade takes effect at the next renewal and gives no credit for the rest of this period (decision 0008, VBD-1). A plan with a smaller disk is unavailable with reason disk_shrink (a disk never shrinks: rebuild onto the smaller plan). blocked says why nothing can be changed now: not_active, cancelling, or upgrade_in_progress.

  • Permission: servers.read
  • API token scope: servers:read
ResponseMeaning
200The plans and where any change stands
defaultError

Example:

curl -X GET "$VEXELIA_URL/api/v1/orgs/$org/servers/$server/plan-options" \
  -H "Authorization: Bearer $VEXELIA_TOKEN"

POST /api/v1/orgs/{org}/servers/{server}/power

Start, stop or restart a server

Queues a start, shutdown, stop or reboot and answers 202 with an operation_id. Refused with server_busy while another action runs on the server.

  • Permission: servers.manage
  • API token scope: servers:power
ParameterInNotes
Idempotency-KeyheaderOptional with an API token: a new string (1 to 100 printable ASCII characters) per logical request. Repeating the request with the same key returns the first answer and does not repeat the action (response header Idempotent-Replay: true); the same key with a different request is 422 idempotency_key_reused. Kept 24 hours, per token and route.

Request body (JSON):

{
  "action": "start"
}
ResponseMeaning
202Queued
defaultError

Example:

curl -X POST "$VEXELIA_URL/api/v1/orgs/$org/servers/$server/power" \
  -H "Authorization: Bearer $VEXELIA_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"action":"start"}'

PUT /api/v1/orgs/{org}/servers/{server}/protect

Mark a server do-not-destroy, or lift the mark

While a server is protected, destroying it, the end of a cancellation and a staff terminate are all refused with server_protected. The mark does not stop a server being suspended for non-payment. Lifting the mark needs a recent step-up (answered step_up_required without one); setting it does not. Repeating the same value is a no-op (changed is false). A token can never call this.

  • Permission: servers.protect
  • API token: not available, use a signed-in session

Request body (JSON):

{
  "protected": false
}
ResponseMeaning
200The flag now
defaultError

PUT /api/v1/orgs/{org}/servers/{server}/rdns

Ask for reverse DNS on one of a server's addresses

Asks for the reverse DNS name of one address of the server (an IPv4 address, or an IPv6 address the server uses). The name must already resolve to the address, an A or AAAA record, or the request is refused with rdns_not_confirmed: reverse DNS only counts for mail when it is forward-confirmed. A null or empty name asks to remove the record. The request goes to a staff queue labelled manual, because no provider reverse-DNS API is connected in the beta; staff set it at the provider and mark it done, and the network tab shows it as pending until then. The record is checked again every day and loses its mail flag, with an email to the owners and billing members, if the forward record goes away. A newer request for the same address replaces a pending one.

  • Permission: servers.manage
  • API token: not available, use a signed-in session

Request body (JSON):

{
  "address": "string",
  "name": "string"
}
ResponseMeaning
202Queued for staff
defaultError

POST /api/v1/orgs/{org}/servers/{server}/rebuild

Rebuild a server onto another image

Destroys everything on the server's disk and installs the chosen image again with the chosen SSH keys. Addresses, plan and billing are unchanged. confirm_name must be the server's name. Refused with rebuild_unavailable unless the server is active, and with server_busy while another action is running. If the chosen image can't boot the server's current firmware, the rebuild switches it. An image without UEFI switches the server to BIOS and turns Secure Boot off, an image without BIOS switches it to UEFI, and an image without Secure Boot turns it off (see supports_uefi, supports_bios and supports_secure_boot on the catalog). The old disk is kept in node trash for 72 hours. The recorded image, keys and hostname change when the node confirms.

  • Permission: servers.manage
  • API token scope: servers:rebuild
  • Step-up: needs a recent password check
ParameterInNotes
Idempotency-KeyheaderOptional with an API token: a new string (1 to 100 printable ASCII characters) per logical request. Repeating the request with the same key returns the first answer and does not repeat the action (response header Idempotent-Replay: true); the same key with a different request is 422 idempotency_key_reused. Kept 24 hours, per token and route.

Request body (JSON):

{
  "confirm_name": "string",
  "hostname": "string",
  "image_id": "00000000-0000-0000-0000-000000000000",
  "ssh_key_ids": [
    null
  ],
  "timezone": "string",
  "user_data": "string"
}
ResponseMeaning
202Queued
defaultError

Example:

curl -X POST "$VEXELIA_URL/api/v1/orgs/$org/servers/$server/rebuild" \
  -H "Authorization: Bearer $VEXELIA_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"confirm_name":"string","hostname":"string","image_id":"00000000-0000-0000-0000-000000000000","ssh_key_ids":[null],"timezone":"string","user_data":"string"}'

GET /api/v1/orgs/{org}/servers/{server}/rescue

Show a server's rescue mode

in_rescue while the server runs the rescue system (its lifecycle is rescue); until is when it goes back to its own disk automatically. available is false when no rescue system exists for the server's architecture.

  • Permission: servers.read
  • API token scope: servers:read
ResponseMeaning
200Rescue state
defaultError

Example:

curl -X GET "$VEXELIA_URL/api/v1/orgs/$org/servers/$server/rescue" \
  -H "Authorization: Bearer $VEXELIA_TOKEN"

POST /api/v1/orgs/{org}/servers/{server}/rescue

Boot a server into the rescue system

Stops the server and boots the signed Vexelia rescue system with the server's own disk attached as a second disk, untouched. The rescue system takes the server's SSH keys and the one-time root password returned here, which is shown only in this response and never stored. The rescue image's signature is checked on the node before the server is stopped. The server returns to its own disk after the staff-set maximum (24 hours by default), with an email an hour before. Refused with server_not_active unless the server is active, server_busy while another action runs, and rescue_unavailable when there is no rescue system for its architecture.

  • Permission: servers.manage
  • API token: not available, use a signed-in session
  • Step-up: needs a recent password check

Request body (JSON):

{}
ResponseMeaning
202Queued; the password is shown only here
defaultError

POST /api/v1/orgs/{org}/servers/{server}/rescue/exit

Boot a server from its own disk again

Stops the rescue system and starts the server from its own disk, keeping whatever was changed on it. The rescue system's disk is deleted. Refused with not_in_rescue unless the server is in rescue.

  • Permission: servers.manage
  • API token scope: servers:power
ParameterInNotes
Idempotency-KeyheaderOptional with an API token: a new string (1 to 100 printable ASCII characters) per logical request. Repeating the request with the same key returns the first answer and does not repeat the action (response header Idempotent-Replay: true); the same key with a different request is 422 idempotency_key_reused. Kept 24 hours, per token and route.

Request body (JSON):

{}
ResponseMeaning
202Queued
defaultError

Example:

curl -X POST "$VEXELIA_URL/api/v1/orgs/$org/servers/$server/rescue/exit" \
  -H "Authorization: Bearer $VEXELIA_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{}'

GET /api/v1/orgs/{org}/servers/{server}/snapshots

A server's snapshots

The server's snapshots, newest first, with the price, the limits, and open_charge, the snapshot charge waiting to be paid, if any (quoted and not yet expired; pay it with POST .../snapshots/charges/{charge}/pay until expires_at).

  • Permission: servers.read
  • API token scope: servers:read
ResponseMeaning
200The server's snapshots
defaultError

Example:

curl -X GET "$VEXELIA_URL/api/v1/orgs/$org/servers/$server/snapshots" \
  -H "Authorization: Bearer $VEXELIA_TOKEN"

POST /api/v1/orgs/{org}/servers/{server}/snapshots

Take a snapshot of a server

Prices a snapshot named name of the server's primary disk. The charge is the snapshot's price for the rest of the period the customer has paid for (remaining over total as an exact duration, rounded half up once; a renewal paid before its period began adds that whole period too), plus tax. The response carries it as charge, which stands for 30 minutes and is paid with POST .../snapshots/charges/{charge}/pay (credit first, then card or 3-D Secure); the snapshot is reserved, billed and queued for the node only after payment settles, and credited back to the wallet if the node cannot take it. A charge below the minimum a card can be charged is added to the next renewal invoice instead (deferred is true and the snapshot is queued at once), and a charge of zero queues the snapshot at once (operation_id is set). The renewal after the paid period bills the snapshot whole. Asking again for the same name while the quote stands returns the same quote. Refused with snapshot_name_taken when a live snapshot of the server has the name, snapshot_limit and snapshot_server_limit at the limits, renewal_charging while the server's renewal is being paid, snapshot_charge_open while another snapshot is waiting to be paid or taken, server_not_active when the server or its node is not available, paid_period_ended when the paid period has ended, server_busy while another action runs. Answers 503 tax_unavailable while tax cannot be worked out yet for the buyer's location; nothing is created or charged.

  • Permission: servers.manage
  • API token: not available, use a signed-in session
  • Step-up: needs a recent password check

Request body (JSON):

{
  "name": "string"
}
ResponseMeaning
202A charge to pay first, or the operation when there was nothing to pay
defaultError

POST /api/v1/orgs/{org}/servers/{server}/snapshots/charges/{charge}/pay

Pay for taking a snapshot

Pays a snapshot charge: wallet credit is held first, then one card intent covers the rest (the client secret is returned for Stripe.js), or the charge settles at once when credit covers it. Settlement issues an invoice, reserves the snapshot (it is billed from then) and voids the renewal that was issued without it; the snapshot is then queued for the node. Repeating the call shows the same intent. quote_expired after 30 minutes (ask again), paid_period_ended if a renewal settled or the period ended since the quote, amount_below_minimum when the card part is below what the processor can charge.

  • Permission: servers.manage
  • API token: not available, use a signed-in session
  • Step-up: needs a recent password check

Request body (JSON):

{
  "payment_method": "00000000-0000-0000-0000-000000000000"
}
ResponseMeaning
200Where the payment stands
defaultError

DELETE /api/v1/orgs/{org}/servers/{server}/snapshots/{snapshot}

Delete a snapshot

Deletes a ready snapshot (queued as one operation) or a failed one (at once; operation_id is then null). There is no credit for the rest of the paid period (decision 0008 applied to snapshots), so the period stands as paid and the next renewal no longer bills it. Refused with snapshot_busy while the snapshot is being taken, restored or deleted, and with server_busy while another action runs on the server.

  • Permission: servers.manage
  • API token: not available, use a signed-in session
  • Step-up: needs a recent password check
ResponseMeaning
202Queued, or done for a failed snapshot
defaultError

POST /api/v1/orgs/{org}/servers/{server}/snapshots/{snapshot}/restore

Restore a snapshot over a server's disk

Puts the snapshot over the server's primary disk: everything written since the snapshot was taken is replaced, and the server is restarted if it was running. confirm_hostname must be the server's hostname. Refused with server_busy while any other action runs on the server (a restore takes the whole disk, so it waits for backups too), snapshot_busy while the snapshot is not ready (being taken, restored or deleted), snapshot_server_changed when the server's disk is no longer the one the snapshot was taken of (it was rebuilt, restored from a backup or from trash, or changed size since), and server_not_active when the server is not active. The restore is queued as one operation; the snapshot reads restoring until the node answers and ready again after, with last_restored_at set when it worked.

  • Permission: servers.manage
  • API token: not available, use a signed-in session
  • Step-up: needs a recent password check

Request body (JSON):

{
  "confirm_hostname": "string"
}
ResponseMeaning
202Queued
defaultError

GET /api/v1/orgs/{org}/servers/{server}/support-grants

Support access given for a server

The most recent grants for the server, open and closed, newest first. status is active, expired or withdrawn. A server of another org is a 404.

  • Permission: servers.read
  • API token scope: servers:read
ResponseMeaning
200Grants
defaultError

Example:

curl -X GET "$VEXELIA_URL/api/v1/orgs/$org/servers/$server/support-grants" \
  -H "Authorization: Bearer $VEXELIA_TOKEN"

POST /api/v1/orgs/{org}/servers/{server}/support-grants

Give support staff time-limited access to a server

The customer's consent for support staff to work on this one server. scope is view, power or console, each including the ones before it. hours is 1 to 168 (seven days), 24 when left out. ticket_id optionally links one of the organisation's tickets. One grant is in force at a time; a second is refused with support_grant_active until the first is withdrawn or ends. The grant is audited and shown to staff on the service. It does not stop staff acting under their own permissions: it records that the customer agreed.

  • Permission: org.manage
  • API token: not available, use a signed-in session

Request body (JSON):

{
  "hours": 0,
  "note": "string",
  "scope": "view",
  "ticket_id": "00000000-0000-0000-0000-000000000000"
}
ResponseMeaning
201Given
defaultError

DELETE /api/v1/orgs/{org}/servers/{server}/support-grants/{grant}

Withdraw support access to a server

Ends the grant at once. Withdrawing a grant that is already withdrawn or expired is a no-op. A grant of another server or org is a 404.

  • Permission: org.manage
  • API token: not available, use a signed-in session
ResponseMeaning
204Withdrawn
defaultError

GET /api/v1/orgs/{org}/servers/{server}/usage

CPU, RAM, disk, network and IOPS of a server

One series per metric over a time range, downsampled to the step. Samples are kept at one-minute resolution for 30 days. A bucket with no sample is a gap: its value and max are null and samples is 0. It is never zero. A gap can mean the server was off, the node was unreachable, or the reading was not available. Buckets start at multiples of the step since the Unix epoch (UTC), and the answer covers whole buckets. Value is the mean over the minutes that have a sample; max is the largest per-minute value. Network is bytes per second and IOPS is reads and writes together per second. Disk is bytes in use as the node measures it (allocated on the node unless a guest agent reports more). A server of another org is a 404.

  • Permission: servers.read
  • API token scope: servers:read
ParameterInNotes
metricqueryA metric to return. Repeat or comma-separate. All metrics when omitted
fromqueryStart of the range (RFC 3339). Default is 24 hours before to
toqueryEnd of the range (RFC 3339)
stepqueryBucket size in seconds, a multiple of 60 up to 86400. Default is the smallest that gives at most 600 points
ResponseMeaning
200Usage series
defaultError

Example:

curl -X GET "$VEXELIA_URL/api/v1/orgs/$org/servers/$server/usage" \
  -H "Authorization: Bearer $VEXELIA_TOKEN"

snapshots

GET /api/v1/orgs/{org}/snapshots

Every snapshot of the organisation's servers, with limits and price

The organisation's snapshots across all its servers (the Snapshots page), newest first, with the server each belongs to, the price per GiB a month, the monthly total of the ones that are billed (a snapshot being deleted is not), and the two limits the organisation works against: snapshots in all, and snapshots on one server (used is then the most any one server has). A snapshot is billed at the size of its server's provisioned primary disk, rounded up to a whole GiB. Snapshots that were deleted are not listed.

  • Permission: servers.read
  • API token scope: servers:read
ResponseMeaning
200Snapshots, limits and price
defaultError

Example:

curl -X GET "$VEXELIA_URL/api/v1/orgs/$org/snapshots" \
  -H "Authorization: Bearer $VEXELIA_TOKEN"

ssh-keys

GET /api/v1/orgs/{org}/ssh-keys

List SSH keys

Lists the organisation's SSH keys with their fingerprints.

  • Permission: sshkeys.read
  • API token scope: sshkeys:read
ResponseMeaning
200Keys
defaultError

Example:

curl -X GET "$VEXELIA_URL/api/v1/orgs/$org/ssh-keys" \
  -H "Authorization: Bearer $VEXELIA_TOKEN"

POST /api/v1/orgs/{org}/ssh-keys

Add an SSH key

Adds an SSH public key in authorized_keys format. Keys are installed on servers at order and rebuild time.

  • Permission: sshkeys.manage
  • API token scope: sshkeys:manage
ParameterInNotes
Idempotency-KeyheaderOptional with an API token: a new string (1 to 100 printable ASCII characters) per logical request. Repeating the request with the same key returns the first answer and does not repeat the action (response header Idempotent-Replay: true); the same key with a different request is 422 idempotency_key_reused. Kept 24 hours, per token and route.

Request body (JSON):

{
  "name": "string",
  "public_key": "string"
}
ResponseMeaning
201Added
defaultError

Example:

curl -X POST "$VEXELIA_URL/api/v1/orgs/$org/ssh-keys" \
  -H "Authorization: Bearer $VEXELIA_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"name":"string","public_key":"string"}'

DELETE /api/v1/orgs/{org}/ssh-keys/{key}

Remove an SSH key

Removes a key from the organisation. Servers already built keep it until they are rebuilt.

  • Permission: sshkeys.manage
  • API token scope: sshkeys:manage
ParameterInNotes
Idempotency-KeyheaderOptional with an API token: a new string (1 to 100 printable ASCII characters) per logical request. Repeating the request with the same key returns the first answer and does not repeat the action (response header Idempotent-Replay: true); the same key with a different request is 422 idempotency_key_reused. Kept 24 hours, per token and route.
ResponseMeaning
204Done
defaultError

Example:

curl -X DELETE "$VEXELIA_URL/api/v1/orgs/$org/ssh-keys/$key" \
  -H "Authorization: Bearer $VEXELIA_TOKEN"

GET /api/v1/orgs/{org}/ssh-keys/{key}

Get an SSH key

Returns one SSH key.

  • Permission: sshkeys.read
  • API token scope: sshkeys:read
ResponseMeaning
200Key
defaultError

Example:

curl -X GET "$VEXELIA_URL/api/v1/orgs/$org/ssh-keys/$key" \
  -H "Authorization: Bearer $VEXELIA_TOKEN"

tickets

GET /api/v1/orgs/{org}/tickets

List the organisation's support tickets

Newest first. Staff-only notes and the staff member who answered are never part of any customer response.

  • Permission: tickets.read
  • API token: not available, use a signed-in session
ParameterInNotes
statusquery
serverqueryOnly tickets about this server
limitquery
beforequeryThe next_before value of the previous page
ResponseMeaning
200One page of tickets
defaultError

POST /api/v1/orgs/{org}/tickets

Open a support ticket

Send JSON, or multipart/form-data with the same fields as text parts and up to three `files` parts (5 MB each; PNG, JPEG, GIF, WebP, PDF or plain text, checked by content). An organisation may have 25 open tickets and may open 10 an hour. Customers choose low, normal or high priority.

  • Permission: tickets.write
  • API token: not available, use a signed-in session

Request body (JSON):

{
  "body": "string",
  "category": "general",
  "priority": "low",
  "server_id": "00000000-0000-0000-0000-000000000000",
  "subject": "string"
}
ResponseMeaning
201The new ticket
defaultError

GET /api/v1/orgs/{org}/tickets/{ticket}

Get a support ticket with its messages

Another organisation's ticket is a 404. Messages are the conversation the customer may see; staff replies are signed "Vexelia support".

  • Permission: tickets.read
  • API token: not available, use a signed-in session
ResponseMeaning
200The ticket and its messages, oldest first
defaultError

GET /api/v1/orgs/{org}/tickets/{ticket}/attachments/{attachment}

Download a ticket attachment

Always sent as a download (`Content-Disposition: attachment`, `nosniff`, sandboxed). An attachment on a staff-only note is a 404.

  • Permission: tickets.read
  • API token: not available, use a signed-in session
ResponseMeaning
200The file
defaultError

POST /api/v1/orgs/{org}/tickets/{ticket}/close

Close a support ticket

Closing a closed ticket changes nothing.

  • Permission: tickets.write
  • API token: not available, use a signed-in session
ResponseMeaning
200The ticket, now closed
defaultError

POST /api/v1/orgs/{org}/tickets/{ticket}/replies

Reply to a support ticket

A reply reopens an answered ticket, and a closed one for 14 days (after that, ticket_closed). JSON or multipart/form-data as when opening a ticket.

  • Permission: tickets.write
  • API token: not available, use a signed-in session

Request body (JSON):

{
  "body": "string"
}
ResponseMeaning
201The message was added
defaultError

transactions

GET /api/v1/orgs/{org}/transactions

List transactions

Money history, newest first. Amounts are signed from the customer's side, so card payments are negative.

  • Permission: billing.read
  • API token: not available, use a signed-in session
ParameterInNotes
beforequeryThe next_before value of the previous page
limitquery
ResponseMeaning
200Transactions
defaultError

transfer

DELETE /api/v1/orgs/{org}/transfer

Cancel the ownership transfer

An owner cancels the open offer; the member is told. Nothing changes hands. No step-up needed. 404 when no offer is open.

  • Permission: org.members.manage
  • API token: not available, use a signed-in session
ResponseMeaning
204Done
defaultError

GET /api/v1/orgs/{org}/transfer

The open ownership transfer

Returns the organisation's open ownership offer as transfer, or null when there is none.

  • Permission: org.members.manage
  • API token: not available, use a signed-in session
ResponseMeaning
200The offer
defaultError

POST /api/v1/orgs/{org}/transfer

Offer ownership to a member

An owner offers the organisation to one of its active members, who gets an email and answers in their own account within 3 days. You choose what you become when it is accepted and confirm that the new owner takes over the organisation's billing. Needs a recent step-up. Only an owner may offer (403), one offer is open at a time (409 transfer_open), and a personal organisation cannot be handed over (409 personal_org).

  • Permission: org.members.manage
  • API token: not available, use a signed-in session
  • Step-up: needs a recent password check

Request body (JSON):

{
  "billing_ack": false,
  "previous_owner_role": "admin",
  "to_user_id": "00000000-0000-0000-0000-000000000000"
}
ResponseMeaning
201Offered
defaultError

trial

GET /api/v1/orgs/{org}/trial

The free trial of this account: whether one is offered, and the one it has

Free trials are off by default and hidden: while the flag trials_enabled is off for this organisation and no staff grant is waiting, the answer is enabled false with no offer and no rules. With trials on (or a grant, or a trial already running) it says whether one can be started now (available), the plan, the number of days, the traffic a trial includes and what keeping the server costs per month (offer), the trial that is running or has just ended (trial) and the earlier ones (history). reason says why none is available: open (one is already running), used (this organisation has had its trial), not_configured (staff have not set a trial plan) or no_card (a card must be on file first). It never says anything about the abuse rules: those are checked, and not explained, when the trial is started.

  • Permission: servers.read
  • API token: not available, use a signed-in session
ResponseMeaning
200The trial state
defaultError

POST /api/v1/orgs/{org}/trial

Start a free trial server

Starts a free trial: a real server on the trial plan for a few days, with a capped amount of traffic, at no charge. Needs the flag trials_enabled for the organisation (or a staff grant), a verified email (email_unverified), a default card on file that the provider gave a fingerprint for (409 trial_needs_card), an unused server slot (limit_reached) and capacity (no_capacity). accept_terms must be true: the customer confirms that the server stops when the trial ends unless they keep it, that nothing renews or charges by itself, and that it is removed a few days after it stops. One trial per organisation, per card and per verified email address; a start that breaks that rule, the per-domain or the per-address limits is 409 trial_not_eligible and does not say which. 409 trial_open when the organisation already has one, 409 trial_unavailable when trials are off or no plan is set. A repeat with the same Idempotency-Key returns the same trial. Nothing is charged and no invoice is made. Audited, refusals included.

  • Permission: servers.manage
  • API token: not available, use a signed-in session

Request body (JSON):

{
  "accept_terms": false,
  "hostname": "string",
  "image_id": "00000000-0000-0000-0000-000000000000",
  "region": "string",
  "ssh_key_ids": [
    null
  ]
}
ResponseMeaning
201The trial and its order
defaultError

POST /api/v1/orgs/{org}/trial/cancel

End the free trial now and destroy its server

Ends the running (or just ended) trial at once: the server is destroyed like any server its owner destroys (its disk stays in node trash for 72 hours) and the subscription closes. It cannot be undone, and it does not give the trial back. Needs confirm_hostname, the server's hostname. Refused with 409 trial_state when there is nothing to end, 409 server_protected for a server marked do-not-destroy, 409 server_busy while another operation runs. Audited.

  • Permission: servers.manage
  • API token: not available, use a signed-in session

Request body (JSON):

{
  "confirm_hostname": "string"
}
ResponseMeaning
200The trial
defaultError

POST /api/v1/orgs/{org}/trial/convert

Keep the trial server: issue its first month

The customer keeping the server, the only way a trial becomes a paid server. The server leaves the trial at once and its first month is issued as an ordinary renewal (proforma_id): it starts when the trial would have ended, or now if the trial has ended, and is paid like any renewal (the due page, the saved card, 3-D Secure); paying it makes a stopped server run again. Nothing is charged by this call itself. If the renewal cannot be priced yet (tax is pending) proforma_id is null and it appears within a minute. Needs billing.manage. 409 trial_state when the trial is not running or ended, 409 account_blocked when the account is blocked for a dispute or debt. Audited.

  • Permission: billing.manage
  • API token: not available, use a signed-in session
ResponseMeaning
200The trial and its first renewal
defaultError

two-factor

GET /api/v1/orgs/{org}/two-factor

Two-factor requirement and who has enrolled

Owner only (anyone else gets 403). The requirement, when it was set and when its grace period ends, and every member with the kind of second factor they hold (authenticator, passkey, both or none) and a status: compliant, not_enrolled (no requirement), in_grace or blocked. Never a secret, a code or a hash. Panel session only.

  • Permission: org.manage
  • API token: not available, use a signed-in session
  • Session only: a person must be present
ResponseMeaning
200The policy and the members
defaultError

PUT /api/v1/orgs/{org}/two-factor

Require two-factor sign-in of every member, or stop requiring it

Owner only, needs a recent step-up, panel session only, audited. With require true every member needs an authenticator app or a passkey. Members without one are mailed at once and reminded a week and a day before the end of the grace period (grace_days, 0 to 90, default 14; 0 applies it now). After it a session of such a member cannot use the organisation (two_factor_enrolment_required) and the API tokens they made for it are refused (the same bare 401 as any refused token), until they have enrolled. The last second factor of a member cannot be removed while the requirement is on (two_factor_required_by_policy). Refused with two_factor_not_enrolled when the owner has none (the switch must not lock out the person who set it) and with personal_org on a personal organisation. When it is already on, a repeat without grace_days changes nothing (no restart, no mail); with grace_days the period is set to that length from now, and one that has already ended is only given back with regrace true (else 409 conflict). The answer is the compliance view.

  • Permission: org.manage
  • API token: not available, use a signed-in session
  • Step-up: needs a recent password check
  • Session only: a person must be present

Request body (JSON):

{
  "grace_days": 0,
  "regrace": false,
  "require": false
}
ResponseMeaning
200The policy as now set, with the members
defaultError

variables

GET /api/v1/orgs/{org}/variables

The organisation's metadata variables

Key and value pairs the organisation's servers can use as ${vexelia.org.KEY} in user-data and startup scripts. Values may be sensitive, so this needs servers.manage and is never cached.

  • Permission: servers.manage
  • API token: not available, use a signed-in session
ResponseMeaning
200The variables
defaultError

POST /api/v1/orgs/{org}/variables

Set a metadata variable

Creates the variable, or replaces the value of the one with that key. The key is lower-case letters, digits and underscores starting with a letter (32 characters at most) and the value is one line of up to 1024 characters. At most 50 variables. The audit log records the key, never the value. A server that already uses the variable gets the new value at its next rebuild.

  • Permission: servers.manage
  • API token: not available, use a signed-in session

Request body (JSON):

{
  "key": "string",
  "value": "string"
}
ResponseMeaning
200Saved
defaultError

DELETE /api/v1/orgs/{org}/variables/{variable}

Delete a metadata variable

Removes the variable. Text that still uses it is not edited; at the next rebuild it is replaced by nothing.

  • Permission: servers.manage
  • API token: not available, use a signed-in session
ResponseMeaning
200Deleted
defaultError

PUT /api/v1/orgs/{org}/variables/{variable}

Change a metadata variable's value

Changes the value of an organisation metadata variable. Its key cannot be changed. Returns not_found for an unknown variable.

  • Permission: servers.manage
  • API token: not available, use a signed-in session

Request body (JSON):

{
  "value": "string"
}
ResponseMeaning
200Saved
defaultError

volume-charges

POST /api/v1/orgs/{org}/volume-charges/{charge}/pay

Pay for buying or growing a volume

Pays a volume charge: wallet credit is held first, then one card intent covers the rest (the client secret is returned for Stripe.js), or the charge settles at once when credit covers it. Settlement issues an invoice, makes the volume or reserves its new size (billed from then) and voids the renewal that was issued without it. A grown volume that holds data is then grown on its host, and the charge is credited back to the wallet if the host cannot do it. Repeating the call shows the same intent. quote_expired after 30 minutes (ask again), paid_period_ended if a renewal settled or the period ended since the quote, amount_below_minimum when the card part is below what the processor can charge.

  • Permission: servers.manage
  • API token: not available, use a signed-in session
  • Step-up: needs a recent password check

Request body (JSON):

{
  "payment_method": "00000000-0000-0000-0000-000000000000"
}
ResponseMeaning
200Where the payment stands
defaultError

volumes

GET /api/v1/orgs/{org}/volumes

Every volume of the organisation, with price and limits

The organisation's volumes (the Volumes page), newest first, with the server that pays for each and the one it is attached to, the price per GiB a month, the largest a volume may be, the monthly total of the ones that are billed (a volume being deleted is not), the volume limit, open_charges (volume charges waiting to be paid: quoted and not yet expired; pay one with POST .../volume-charges/{charge}/pay until expires_at) and servers, the active servers a volume can be bought for or attached to, with the host each runs on. A volume is one block disk on one host: it can be attached only to servers on that host once it holds data. Volumes that were deleted are not listed.

  • Permission: servers.read
  • API token scope: servers:read
ResponseMeaning
200Volumes, limits and price
defaultError

Example:

curl -X GET "$VEXELIA_URL/api/v1/orgs/$org/volumes" \
  -H "Authorization: Bearer $VEXELIA_TOKEN"

POST /api/v1/orgs/{org}/volumes

Buy a volume

Prices a new blank volume named name of size_gib, bought for the server server_id, whose subscription pays for it. The charge is the volume's price for the rest of the period the customer has paid for (remaining over total as an exact duration, rounded half up once; a renewal paid before its period began adds that whole period too), plus tax. The response carries it as charge, which stands for 30 minutes and is paid with POST .../volume-charges/{charge}/pay (credit first, then card or 3-D Secure); the volume exists and is billed only after payment settles. A charge below the minimum a card can be charged is added to the next renewal invoice instead (deferred is true and the volume exists at once), and a charge of zero makes the volume at once (charge is null). The renewal after the paid period bills the volume whole, on that server's subscription. The volume holds no data on any host until it is first attached; that attach decides which host it lives on. Asking again for the same name and size while the quote stands returns the same quote. Refused with volume_name_taken when a volume has the name, volume_limit at the limit, a 422 on size_gib outside 1 GiB to the largest size, renewal_charging while the server's renewal is being paid, volume_charge_open while another volume charge is waiting to be paid, server_not_active when the server is not an active VPS, paid_period_ended when the paid period has ended. Answers 503 tax_unavailable while tax cannot be worked out yet for the buyer's location; nothing is created or charged.

  • Permission: servers.manage
  • API token: not available, use a signed-in session
  • Step-up: needs a recent password check

Request body (JSON):

{
  "name": "string",
  "server_id": "00000000-0000-0000-0000-000000000000",
  "size_gib": 0
}
ResponseMeaning
202A charge to pay first, or the volume when there was nothing to pay
defaultError

DELETE /api/v1/orgs/{org}/volumes/{volume}

Delete a volume

Deletes a volume that is not attached. A volume that holds no data is deleted at once (operation_id is null); one with data is queued for its host as one operation, which moves the data to the host's trash for 72 hours (nothing is destroyed at once). There is no credit for the rest of the paid period (decision 0008 applied to volumes), so the period stands as paid and the next renewal no longer bills it. Refused with volume_host_unsupported when the volume has data and its host has not reported that it runs volumes, volume_attached while a server holds it (detach it first), volume_busy while it is changing, and server_busy while another action runs on the server that last held it.

  • Permission: servers.manage
  • API token: not available, use a signed-in session
  • Step-up: needs a recent password check
ResponseMeaning
202Queued, or done for a volume with no data
defaultError

GET /api/v1/orgs/{org}/volumes/{volume}

One volume

One volume of the organisation, as the list shows it. A deleted volume is a 404.

  • Permission: servers.read
  • API token scope: servers:read
ResponseMeaning
200The volume
defaultError

Example:

curl -X GET "$VEXELIA_URL/api/v1/orgs/$org/volumes/$volume" \
  -H "Authorization: Bearer $VEXELIA_TOKEN"

POST /api/v1/orgs/{org}/volumes/{volume}/attach

Attach a volume to a server

Attaches an available volume to the server server_id in its lowest free extra-disk slot (a server has four, shared by its plan's extra disks and volumes), live if the server runs. The first attach makes the volume's disk on the server's host (it must have room) and fixes the host; later attaches need a server on that same host. Refused with volume_attached when it is attached already, volume_busy while it is changing, volume_other_node when the server is on another host, volume_no_slot when the server's four slots are used, server_not_active when the server is not an active VPS, renewal_charging while its renewal is being paid, node_full when the host has no room, volume_host_unsupported when the host has not reported that it runs volumes (an older host software version), server_busy while another action runs on the server. The attach is one operation: the volume reads attaching until the host answers, attached after, and available again with error_code if it could not.

  • Permission: servers.manage
  • API token: not available, use a signed-in session
  • Step-up: needs a recent password check

Request body (JSON):

{
  "server_id": "00000000-0000-0000-0000-000000000000"
}
ResponseMeaning
202Queued
defaultError

POST /api/v1/orgs/{org}/volumes/{volume}/detach

Detach a volume from its server

Detaches the volume from the server that holds it. The volume and its data stay on the host, ready to be attached again, and nothing goes to trash. The host refuses while the guest may be using the disk (a mounted filesystem, or no guest agent to ask) unless force is true, and a server that is not running can only detach with force (disk_force_required): nothing can then check whether the guest uses it. Detaching gives no credit. Refused with volume_host_unsupported when the host has not reported that it runs volumes, volume_not_attached when no server holds it, volume_busy while it is changing, server_busy while another action runs on the server.

  • Permission: servers.manage
  • API token: not available, use a signed-in session
  • Step-up: needs a recent password check

Request body (JSON):

{
  "force": false
}
ResponseMeaning
202Queued
defaultError

POST /api/v1/orgs/{org}/volumes/{volume}/resize

Grow a volume

Prices growing the volume to size_gib: the charge is the price of the extra GiB for the rest of the paid period, as for buying one, and is paid with POST .../volume-charges/{charge}/pay. A volume only grows (a 422 on size_gib otherwise, and above the largest size). A volume that holds no data takes the new size at once when paid; one with data is grown on its host (online when attached to a running server) after payment, and the charge is credited back to the wallet if the host cannot do it. A volume that is attaching, detaching, being grown or deleted is volume_busy, and a volume whose host has not reported that it runs volumes is volume_host_unsupported (nothing is quoted).

  • Permission: servers.manage
  • API token: not available, use a signed-in session
  • Step-up: needs a recent password check

Request body (JSON):

{
  "size_gib": 0
}
ResponseMeaning
202A charge to pay first, or the change when there was nothing to pay
defaultError

webhook-event-types

GET /api/v1/orgs/{org}/webhook-event-types

List the webhook event types

The catalog is versioned. `version` is the payload schema version sent in every payload; a breaking change adds a new version and keeps the old one for existing endpoints. Every event type belongs to one category (servers, backups, billing, support, status), and `categories` lists them with their types; an endpoint can follow a whole category instead of picking its types one by one.

  • Permission: webhooks.read
  • API token: not available, use a signed-in session
ResponseMeaning
200The event types a webhook endpoint can select
defaultError

webhooks

GET /api/v1/orgs/{org}/webhooks

List webhook endpoints

Lists the organisation's webhook endpoints. Secrets are not shown.

  • Permission: webhooks.read
  • API token: not available, use a signed-in session
ResponseMeaning
200Endpoints, oldest first. Secrets are never listed
defaultError

POST /api/v1/orgs/{org}/webhooks

Add a webhook endpoint

The signing secret is in the response and nowhere else; store it now. Requests carry `Vexelia-Signature: t=<unix>,v1=<hex>`, where v1 is HMAC-SHA256 over `<t>.<raw body>` keyed with the secret. Reject a timestamp more than five minutes from your clock, and skip event IDs you have already handled. The URL must be https, on port 443 or 8443 (or no port), and reachable from the internet; addresses in private, loopback, link-local and metadata ranges are refused at every attempt.

  • Permission: webhooks.manage
  • API token: not available, use a signed-in session
  • Step-up: needs a recent password check

Request body (JSON):

{
  "categories": [
    null
  ],
  "description": "string",
  "events": [
    null
  ],
  "url": "string"
}
ResponseMeaning
201The endpoint and its secret
defaultError

DELETE /api/v1/orgs/{org}/webhooks/{webhook}

Delete a webhook endpoint

Deletes the endpoint and its delivery log.

  • Permission: webhooks.manage
  • API token: not available, use a signed-in session
ResponseMeaning
204Done
defaultError

GET /api/v1/orgs/{org}/webhooks/{webhook}

Get a webhook endpoint

Returns one webhook endpoint. The secret is not shown. Returns not_found for an unknown endpoint.

  • Permission: webhooks.read
  • API token: not available, use a signed-in session
ResponseMeaning
200The endpoint
defaultError

PATCH /api/v1/orgs/{org}/webhooks/{webhook}

Change a webhook endpoint, or switch it on or off

Send only what changes. `enabled` switches the endpoint on or off; switching it off gives up what was queued for it. A switched-off endpoint is not sent to. Changing `url` needs step-up (step_up_required without it); other changes do not.

  • Permission: webhooks.manage
  • API token: not available, use a signed-in session

Request body (JSON):

{
  "categories": [
    null
  ],
  "description": "string",
  "enabled": false,
  "events": [
    null
  ],
  "url": "string"
}
ResponseMeaning
200The endpoint
defaultError

GET /api/v1/orgs/{org}/webhooks/{webhook}/deliveries

Delivery log of a webhook endpoint

Newest first, with status, attempts and the last response code. Response bodies are not kept.

  • Permission: webhooks.read
  • API token: not available, use a signed-in session
ResponseMeaning
200Deliveries
defaultError

POST /api/v1/orgs/{org}/webhooks/{webhook}/deliveries/replay-failed

Send the events that could not be delivered again

Queues a new delivery for every event of this endpoint that ended as a dead letter and has not been sent again since, oldest failure first, at most 100 per request (`more` is true only if others remain); an event that was ever delivered to the endpoint is not replayed. The receiver sees the same event IDs. Only while the endpoint is on; counts toward the endpoint's manual-send limit.

  • Permission: webhooks.manage
  • API token: not available, use a signed-in session
ResponseMeaning
202Queued
defaultError

POST /api/v1/orgs/{org}/webhooks/{webhook}/deliveries/{delivery}/replay

Send a delivery's event again

Queues the same event as a new delivery; the receiver sees the same event ID. Only a delivered or dead delivery can be replayed, and only while the endpoint is on.

  • Permission: webhooks.manage
  • API token: not available, use a signed-in session
ResponseMeaning
202Queued
defaultError

POST /api/v1/orgs/{org}/webhooks/{webhook}/rotate-secret

Issue a new signing secret

The old secret keeps signing for `overlap_hours` (24 by default, at most 72), and both signatures are sent meanwhile, so the receiver can switch without a gap.

  • Permission: webhooks.manage
  • API token: not available, use a signed-in session
  • Step-up: needs a recent password check

Request body (JSON):

{
  "overlap_hours": 0
}
ResponseMeaning
200The endpoint and its new secret
defaultError

POST /api/v1/orgs/{org}/webhooks/{webhook}/test

Send a test event

Queues a `webhook.test` event to this endpoint only. Refused with webhook_disabled while the endpoint is off.

  • Permission: webhooks.manage
  • API token: not available, use a signed-in session
ResponseMeaning
202Queued
defaultError