Changed
GET /api/v1/services/{id}/backupsandGET /api/v1/apps/{uuid}/addons/{id}/backupsseparate the setting from the cluster.backup_enabledused to report what the cluster runs, which trails an update, so backups you had just enabled read back asfalse. It now reports the configured setting and changes as soon asPUTaccepts it. The newbackup_activefield carries what the cluster runs.backup_enabled: truewithbackup_active: falsemeans the change is still being applied.backup_enabledis alwaysfalseon the free plan.backup_enabled,public_access,s3_enabledandto_new_clusterare booleans. OnPOSTandPUT /api/v1/services,POSTandPUT /api/v1/apps/{uuid}/addonsand bothrestore_backupendpoints they accept JSONtrueandfalse.backup_enabled,public_accessands3_enabledused to refuse them with400.'1'and'0'are still accepted.
Fixed
to_new_cluster: trueonrestore_backuprestores into a new cluster. Only the string'1'did.truewas accepted and treated asfalse, so the restore replaced the current database instead of creating a new one alongside it.
Changed
- Cloning an application takes
apps:manage.POST /api/v1/apps/{uuid}/cloneanswered toapps:deploy,apps:operateorapps:manage, as the 2026-08-29 entry below records. Creating an application already takesapps:manage, and the dashboard only ever offered cloning to it, so a role or token without it now gets403. - A preview environment config must run on a resource that accepts its project. While
enabledis true,PUT /api/v1/apps/{uuid}/preview_environments/configrefuses a resource assigned to projects other than the target with422. That applies toresource_id, and to the application’s own resource inparentmode.
Fixed
- Cloning onto a resource somebody else created no longer fails with
Invalid resource_id. Only resources the caller had created themselves were accepted, whatever their role in the workspace.
Changed
- An application’s
statecan readsleeping. A free-plan application that has had no traffic for 30 minutes is scaled to zero, andGET /api/v1/apps/{uuid}reportssleepinguntil something wakes it. The request that wakes it is answered with503while the application starts, which is a wake-up in progress rather than a failure.PATCH /api/v1/apps/{uuid}/stateacceptsschedule_startandschedule_stopon a sleeping application.
Fixed
- A free application waking on traffic no longer sends
app_state_changed. It arrived asstartedwith no stop before it. Going to sleep and waking up now send nothing, so the event still means somebody started, stopped or restarted the application.
Changed
Permissions on a number of endpoints moved, so calls that answered403 for a role may now succeed. Nothing about the requests or the responses changed.- Private networks. Subnets (
POST/DELETE /api/v1/vpcs/{uuid}/subnets), peer routes (POST,POST .../apply,DELETE /api/v1/vpcs/{uuid}/peer_routes) andPUT /api/v1/vpcs/{uuid}moved fromnetwork:managetonetwork:operate. They cost nothing and are undone by the role that made them, which is whatnetwork:operateis for. Creating and deleting a VPC, and switching a VPN gateway on or off, stay onnetwork:manage. - Site-to-site tunnels are no longer restricted to the workspace owner:
POSTandDELETE /api/v1/vpcs/{uuid}/site_connectionsnow takenetwork:manage. The address pool and the $199/mo charge are unchanged, and a region with no free address still answers422. - Application ports.
POST,DELETE /api/v1/apps/{uuid}/portsand theexpose_publicly/make_privateactions moved fromapps:managetoapps:operate, matching add-ons and cron jobs — the other things that live inside an application. A public port is an endpoint on a shared proxy: it costs nothing, and the role that takes one gives it back. - Preview environments.
GETandPUT /api/v1/apps/{uuid}/preview_environments/configmoved fromapps:managetoapps:operate;POST .../preview_environments/{uuid}/redeploymoved toapps:deploy, which is what a rollback and a cancel already take. Deleting a preview environment stays onapps:manage. PUT /api/v1/static_sites/{uuid}moved fromapps:managetoapps:operate, so a static site’s settings answer like any other application’s.- An application’s configuration answers to the permissions that run it.
PUT /api/v1/apps/{uuid},PUT /api/v1/apps/{uuid}/securityandPUT /api/v1/apps/{uuid}/deploymenttakeapps:deploy,apps:operateorapps:manage;/varsand/secret_filestakeapps:operateorapps:manage;PUT /api/v1/static_sites/{uuid}/deploymenttakesapps:operate. All of them previously requiredapps:manage. - Public access is
operatein both directions.public_accesson an add-on or onPUT /api/v1/services/{id}, and a port created withpublic: true, answer toapps:operate/services:operatelike the rest of the resource they belong to. A public port is an endpoint on a shared proxy: it costs nothing, and the role that takes one gives it back. - Ending an add-on’s data stays on
apps:manage.DELETE /api/v1/apps/{uuid}/addons/{id}andPOST .../reset_databasedid not move down with the rest of the add-on endpoints, andPOST /api/v1/services/{id}/reset_databasemoved up toservices:manage: neither the add-on nor the rows in it come back.POST .../restore_backupstays onoperate— a restore is what an incident needs. - TLS certificates answer like the domain they serve. The certificate endpoints take
apps:manage, alongside domains. - Add-ons and cron jobs are
apps:operate.POST,PUT /api/v1/apps/{uuid}/addonsandPATCH .../addons/{id}/state, along with every/cronjobsendpoint, moved down fromapps:manage. An add-on carves its quota out of a resource the workspace has already paid for, so nothing here reaches the subscription — and resetting or restoring its database, the destructive part, was alreadyapps:operate. - Reading a cron job takes
apps:view.GET /api/v1/apps/{uuid}/cronjobs,GET .../cronjobs/{id}and its log stream answeredapps:operate, which was stricter than the dashboard and stricter than every other read. Creating, changing and deleting one stays onapps:operate. - Deleting a project no longer requires having created it.
DELETE /api/v1/projects/{uuid}takesprojects:manage, so a workspace owner can remove a project somebody else made — the dashboard never asked for more./varsand/secret_fileson a project now acceptprojects:manageas well asprojects:operate. - Buckets now answer the same permission here as they do in the dashboard.
PUT /api/v1/buckets/{uuid}moved frombuckets:managetobuckets:operate— the disk allocation included — because a bucket’s settings areoperateeverywhere else.POST /api/v1/buckets/{uuid}/objects/download_urlis unchanged atbuckets:view; it was the dashboard that asked for more, and it no longer does. Uploading, renaming and deleting objects stay onbuckets:operate.
Fixed
-
A public port answers with its address instead of
500.PATCH /api/v1/apps/{uuid}/ports/{port_id}/expose_publiclyandPOST /api/v1/apps/{uuid}/portswithpublic: truefailed outright: the response described the proxy the port landed on as a nestedport_endpointobject, and building it raised. Those fields are now flat on the port —endpoint_id,endpoint_hostname,endpoint_ipv4_primary,endpoint_ipv4_secondary,endpoint_ipv6_primary,endpoint_ipv6_secondary— alongsidepublic_endpoint(hostname:port) andexternal_port. Read the address off the response: the external port is assigned from a pool and is never the internal one. -
Seven endpoints that enforced no permission at all now enforce one.
GET /api/v1/appsandGET /api/v1/apps/{uuid}/activitytakeapps:view;POST /api/v1/apps/{uuid}/deployandPATCH /api/v1/apps/{uuid}/statetakeapps:deploy;POST /api/v1/apps/{uuid}/clone,PUT /api/v1/apps/{uuid}/health_checksandPUT /api/v1/apps/{uuid}/scaling_profiletakeapps:deploy,apps:operateorapps:manage. Until now any workspace member reached them, and a workspace token limited to:viewpermissions could deploy a commit, stop an application and raise its replica count. Those calls answer403from now on, so check the permissions on any token you issued for read-only use before they start failing.
Changed
- Webhook payloads name the resource an event came from.
app_state_changed,app_unhealthy,app_crash_loop,app_stopped,app_blockedandscaling_limit_reachedcarrydata.resource_nameanddata.labels— the labels set on that resource, an empty array where there are none. Routing on the environment an application runs in no longer needs a second call to look it up. deploy_endedcarriesdata.message, the reason a deployment failed, in the platform’s own wording where it recognises the underlying error. It isnullon a deployment that did not fail.app_state_changedreports a requested restart.data.stateis one ofstarted,stopped,failedorrestart_scheduled. A restart now delivers two events rather than none:restart_scheduledwhen it is asked for, andstartedonce the application is running again. Subscribers filtering onstateshould expect the new value.
Added
- VPCs: private networks a workspace’s workloads join to reach each other by name, with their own
network:*permissions — read isnetwork:view, attach and detach arenetwork:operate, everything else isnetwork:manage.GET /api/v1/vpcsandPOST /api/v1/vpcsto list and create.region_idis required;namedefaults todefaultandcidr_v4to10.224.0.0/16. A range that is not RFC1918, is smaller than/22, or overlaps a platform network is refused with422naming the conflict. A VPC belongs to one region and cannot span regions.GET /api/v1/vpcs/{uuid}returns the network includingresolver_v4, the VPC’s own DNS resolver, andvpn_pool_v4. There is no update endpoint: name, label and range are fixed once created.DELETE /api/v1/vpcs/{uuid}removes the VPC, its router and its resolver. Refused with422while any subnet remains.GET,POST /api/v1/vpcs/{uuid}/subnetsandDELETE /api/v1/vpcs/{uuid}/subnets/{subnet_uuid}manage the ranges carved out of the VPC.familyisdualby default, oripv4for no IPv6 at all andipv6for no IPv4;cidr_v4is required unless the family isipv6, andcidr_v6is derived from the VPC’s own when omitted.publicdefaults totrueand is the only thing that distinguishes two subnets: public keeps the platform default route, private moves it to the VPC. Deleting is refused with422while a workload is still attached.GET,POST /api/v1/vpcs/{uuid}/attachmentsandDELETE /api/v1/vpcs/{uuid}/attachments/{attachment_uuid}attach and detach workloads. Attaching takessubnet_uuid,attachable_type(App,ServiceorAddon) andattachable_uuid, and is refused with422when the workload runs in a different region from the VPC.
vpc_uuidandvpc_subnet_uuidonPOST /api/v1/apps: join a VPC when the application is created instead of attaching afterwards, which avoids the restart an attach costs.vpc_subnet_uuidimplies its VPC, sovpc_uuidcan be omitted. The VPC must be in the same region asresource_id.sub_pathon shared storage: accepted onPOST /api/v1/apps/{uuid}/addonsfor thestoragetype and onPOST /api/v1/services/{id}/mount_app. It mounts a subdirectory of the volume instead of its root, creating it if missing, so several applications can share one volume without seeing each other’s files. It must be a relative path —.and..segments and backslashes are refused — and RWX (shared) access only.- VPN access to a VPC, so a private network can be reached from outside the platform. All under
network:*permissions, except site-to-site which needs the workspace owner.GET,POST /api/v1/vpcs/{uuid}/vpn_gatewaysandDELETE /api/v1/vpcs/{uuid}/vpn_gateways/{gateway_uuid}enable a terminator.kindiswireguard,tailscaleorcloudflare, one of each per VPC. Cloudflare needsorganization,client_idandclient_secret; Tailscale needsauth_key. Those credentials are stored encrypted and are never returned by any endpoint. IPsec is deliberately absent — it comes up with the first site-to-site connection. Disabling is refused with422while site-to-site connections still terminate on the gateway.GET,POST /api/v1/vpcs/{uuid}/vpn_usersandDELETE /api/v1/vpcs/{uuid}/vpn_users/{user_uuid}manage WireGuard devices.POSTrequires WireGuard to be enabled first, or answers422.GET /api/v1/vpcs/{uuid}/vpn_users/{user_uuid}/configreturns the device’s WireGuard configuration file. It can be read exactly once. The private key is never stored — it is held in a 15-minute vault that this read empties — so a second call answers410, and so does a call made before the device isactive. A device whose configuration expired unread cannot be recovered; delete it and add another.GET,POST /api/v1/vpcs/{uuid}/peer_routes,POST /api/v1/vpcs/{uuid}/peer_routes/applyandDELETE /api/v1/vpcs/{uuid}/peer_routes/{route_uuid}stage and apply the ranges reachable behind a terminator.gateway_kindtakes any of them, though in practice only Cloudflare WARP and Tailscale need a route — WireGuard’s client pool and an IPsec connection’s remote selectors are derived from the gateways themselves. Creating a route changes nothing on its own; applying restarts every workload attached to the VPC, which is why they are staged and sent together.applyanswers422when nothing is staged.GET,POST /api/v1/vpcs/{uuid}/site_connectionsandDELETE /api/v1/vpcs/{uuid}/site_connections/{connection_uuid}manage IPsec tunnels to a remote network. Each tunnel permanently holds one of the region’s public IPv4 addresses and bills $199/mo, so only the workspace owner may call these — everyone else gets403— and a region with no free address answers422rather than queueing. Readpublic_ipoff the response: that is the address to configure on the far side. The pre-shared keys sent intunnelsare stored encrypted and never returned.
PUT /api/v1/vpcs/{uuid}changes a VPC’slabelandis_default. The name and the address ranges are fixed at creation, because the platform derives the whole address plan from them on every action. Settingis_defaultwhile another VPC in that region holds it is refused with422rather than moving it aside.
Changed
- A new VPC answers
pending.POST /api/v1/vpcsreturns before the platform has derivedcidr_v6,resolver_v4andvpn_pool_v4, so all three come backnull. PollGET /api/v1/vpcs/{uuid}untilstatusisactive, then read them. - A new VPC already has a subnet. The platform creates one named
default, a/24taken after the block it keeps for the router, the resolver and the VPN terminators —10.224.1.0/24on a10.224.0.0/16VPC. ListGET /api/v1/vpcs/{uuid}/subnetsbefore creating one — attaching a workload usually needs no subnet call at all, and carving10.224.1.0/24by hand is refused with422for overlapping the subnet that is already there. - Attaching a workload restarts it. An interface cannot be added to a running pod, so attaching an addon means a database restart. Detaching restarts it again.
- An attachment’s
fqdnis a name, not a fixed address. It resolves to the workload’s pod address, so it goes stale after a rollout until the next attach, detach or peer-route change. A cron job gets nofqdnat all. POST /api/v1/apps/{uuid}/cronjobsrequireslabelandcommand, pluscronwhenschedule_typeis"cron".schedule_typenow defaults to"interval". Previouslynamewas the only required field whilelabelandcommandwere documented as optional — omitting either failed the call, and anamesent with the request is discarded, since the identifier the job runs under is generated and returned asname.visibilityonPOST /api/v1/bucketsandPUT /api/v1/buckets/{uuid}is validated againstprivate_accessandpublic_access. Any other value is now rejected with400before it reaches the bucket, instead of422carrying a raw internal message.- Mounting the same shared storage service to the same application twice is refused with
422.POST /api/v1/services/{id}/mount_appused to create a second storage addon with its own mount point, andunmount_appthen removed only one of the two, because it matches a single addon by service. To move a mount, unmount it and mount it again at the new path.
Fixed
- An application listing no longer fails because one add-on is still being provisioned.
GET /api/v1/apps,GET /api/v1/projects/{project_id}/appsand every response carrying an add-on answered500for the whole workspace while any add-on existed without its configuration row — the window between creating a database and its credentials being written.connection_detailsnow comes back{}for an add-on in that window, the same as for shared storage, which has no connection to report. - Port refusals say what is wrong instead of answering
500.POST /api/v1/apps/{uuid}/ports,DELETE /api/v1/apps/{uuid}/ports/{port_id},PATCH .../expose_publiclyandPATCH .../make_privateanswered500for every ordinary refusal — creating a port that already exists, or exposing one in a region with no free endpoint. They now answer422with the reason inerrors. - A malformed SSH public key is rejected, not a
500.POST /api/v1/users/me/ssh_keysanswered500when the key was the right shape but the wrong bytes — a truncated paste, or key material of the wrong length. It now answers422naming the problem. - Bucket object keys with a file extension are addressable again.
DELETE /api/v1/buckets/{uuid}/objects/{key}andPUT /api/v1/buckets/{uuid}/objects/{key}/renameread the extension as a response-format suffix and answered with a page-not-found for any real filename,docs%2Freport.pdfincluded. Keys with no dot were unaffected.
Added
app_state_changedwebhook event: fires when somebody starts, stops, or restarts an application.app_stoppeddoes not cover this — that one fires when the platform stops an application after repeated crashes, and the two stay separate events.include_review_appson webhooks: accepted onPOST /api/v1/webhooksandPUT /api/v1/webhooks/{uuid}, and returned on every webhook response. A preview environment is an application in its own right with its own UUID, so a webhook narrowed withapp_uuidsreceives nothing from the preview environments of those applications. Set it totrueto have their events matched against the parent as well. Defaults tofalse, so a webhook that did not ask for pull-request traffic does not begin receiving it. A webhook covering every application in the workspace already receives them.
Changed
- A new account gets a 30-day free trial on
miget_hobby_5.POST /api/v1/resourcesreturns the usualcheckout_url, but for an account that has never paid for a resource and has not used a trial before, buying that plan on its own, with nocomponents, opens a checkout that charges nothing until the trial ends. Billing then starts at the plan’s normal price.PUT /api/v1/resources/{uuid}behaves the same way when it is the workspace’s first paid change. - The trial needs a saved payment card, and the request is rejected with
422without one — or when that card has already been used for a trial. This is the rule the free plan already has, now reaching a paid plan. Relay the rejection rather than retrying without the trial: the plan would then be charged immediately. - Any other plan is an ordinary purchase, cheaper ones included, and so is
miget_hobby_5with a paid component on the same request. There is no partial version of the trial. - Buying more during a trial ends it. Adding a resource or moving up to a dearer plan closes the trial that day and bills the new total; a downgrade or a sideways move leaves it running to its original end date.
Added
- Assign a resource to projects: a resource is shared by the whole workspace until it is assigned, after which only the projects it is assigned to may place workloads on it. Anything else is refused with
422.POST /api/v1/projects/{project_id}/resourceswithresource_idassigns one. Needs bothprojects:manageandresources:manage.DELETE /api/v1/projects/{project_id}/resources/{resource_id}returns it to the shared pool. Needs onlyprojects:manage— removing an assignment only ever widens access, and every workload already there stays legal.- Assigning is refused with
422while the resource still runs workloads from a project outside the resulting list. The error names those projects and their workload counts, except the ones you cannot reach, which it reduces to a count. Sendwith_hosting_projects: trueto assign the resource to those projects too and let the call through; when a blocking project is redacted this is the only way forward, since you cannot name a project you cannot see.
- Assignment on the resource response: every resource carries
assigned(boolean) andproject_ids(project UUIDs).assigned: falsemeans any project may deploy on it. Read both before offering aresource_id: the resource is usable whenassignedis false, or whenproject_idscontains the target project.project_idslists only projects you can access. - Project access: a project can be closed to a list of members and roles, returned as
restrictionson the project response. Empty means the project is open to the whole workspace.POST /api/v1/projects/{project_id}/restrictionswith exactly one ofuser_emailorrole_name; sending both or neither is a400. Adding the first entry is what closes the project.DELETE /api/v1/projects/{project_id}/restrictions/{id}removes one entry, where{id}is the numericidfrom therestrictionslist. Removing the last one reopens the project.- Both need
projects:manage, and are available on organization and enterprise workspaces only. The plan refusal is checked before the subject, so on a smaller plan you get the plan message even when the email is also wrong.
- Move a workload between projects:
project_idonPUT /api/v1/apps/{uuid},PUT /api/v1/static/{uuid},PUT /api/v1/services/{id},PUT /api/v1/stacks/{uuid}andPUT /api/v1/buckets/{uuid}moves it to another project while it keeps running on the same resource. Moving a stack moves every application and service in it. Moving needs manage-level permission (apps:manage,services:manage), not the operate level the rest of the update takes. - Buckets can belong to a project:
project_idis accepted onPOST /api/v1/bucketsandPUT /api/v1/buckets/{uuid}. It is never inferred — omit it and the bucket has no project, even in a workspace with exactly one. Sendnullon update to clear it. - Workspace API tokens: tokens created under Settings → Developers can now be scoped to a workspace instead of a person. A workspace token carries its own permission list and its own project list, and nothing else is consulted — not the role of whoever created it, not workspace ownership. It is pinned to its workspace (an
X-Workspace-Idnaming a different one is refused with403), cannot reach/api/v1/users/meor anything under it, and cannot be granted workspace administration, so it can never mint another token. Tokens are managed in the dashboard only; there is no endpoint for them.
Changed
- Webhook endpoints require
workspace:general, notworkspace:integrations. API tokens and webhooks moved to their own Developers settings tab, and the permission moved with them. - A project you cannot reach is invisible rather than forbidden. Restricted projects are absent from
GET /api/v1/projects, and so are their applications, static sites, services, stacks and buckets in their own listings. Addressing any of them by UUID returns404, never403. The same applies to resources assigned solely to projects you cannot reach; an unassigned resource stays visible to everyone. A short list is no longer proof that the workspace holds nothing else. - A
project_idyou cannot reach and one that does not exist answer identically:404with{"error": "Project not found"}, on every endpoint that accepts a move. Telling them apart would confirm a restricted project is there. Read this as “the destination could not be resolved” — the workload is untouched and still in its original project. - Creating or updating a resource no longer falls back to ownership.
PUT /api/v1/resources/{uuid}andPUT /api/v1/resources/{uuid}/labelspreviously allowed the user who created the resource through regardless of role; they now requireresources:operatelike every other caller.
Fixed
- The permission reference was wrong in both directions. It listed
apps:create,resource:view,resource:manageandworkspace:settings, none of which exist, and omitted everyservices:permission along withprojects:operateand five of the sevenworkspace:ones. A403can now be turned into the name of a permission that can actually be granted.
Added
- Static sites: a new resource type with its own endpoints under
/api/v1/static. A static site serves prebuilt HTML, CSS and JavaScript from object storage — there is no compute resource, no replicas, no ports and no environment variables. It is not an application and is not reachable through/api/v1/apps.GET /api/v1/staticandPOST /api/v1/staticto list and create.GET,PUTandDELETE /api/v1/static/{uuid}to read, rename or move between projects, and delete the site with its content and domains.PUT /api/v1/static/{uuid}/deploymentto update build and routing settings. Applied as a patch: fields you omit keep their stored value.source_typeis not accepted here.POST /api/v1/static/{uuid}/deploymentsto deploy — send a multipartarchive(zip, max 1 GB) for azipsite, or omit it to rebuild agithubsite. Returns409 Conflictwhile a deployment is in flight.GET /api/v1/static/{uuid}/deploymentsfor deployment history.POST /api/v1/static/{uuid}/deployments/{id}/cancelto stop a build that is still running. Onlygit_pushandgithubsites run a build; aziporsftpdeployment is a direct sync with nothing to interrupt.POST /api/v1/static/{uuid}/deployments/{id}/rollbackto republish the site from the commit that deployment shipped.githubonly — a static site produces no image, so there is no stored artifact to redeploy and the platform rebuilds that commit instead. Agit_pushsite is rolled back by pushing again, andzipandsftpdeployments carry no commit; both return422.GET /api/v1/static/{uuid}/filesto browse deployed content one directory level per call (prefix,limit,cursor). Read-only. Returns422while the site’s storage is still being provisioned.GET|POST /api/v1/static/{uuid}/domains,PUT|DELETE .../domains/{domain_uuid}andPOST .../domains/{domain_uuid}/verifyfor custom domains, in the same shape as an app’s.
- Static site content sources:
deployment_config.source_typeis required at creation and is one ofgithub,git_push,ziporsftp. It decides what gets provisioned and cannot be changed afterwards — switching means creating another site.githubandgit_pushare built for you with the generator auto-detected;build_command,output_dirandproject_pathoverride detection, andspa_moderewrites unknown paths to/index.html.zipandsftptake already-built output. - Connection details come back ready to use:
deployment_config.git_ssh_urlis the remote agit_pushsite is pushed to, and appears once its repository has been provisioned — poll for it before pushing.deployment_config.sftp_endpointis the completeuser@hosttarget for ansftpsite. Read both from the site response rather than assembling them from the name and region. - Static site names are exact: unlike applications, no random suffix is appended. The name you send is the name the site is served under, and a name already in use is rejected with
422rather than silently renamed. - Static sites are created in
eu-east-1only:regiononPOST /api/v1/staticaccepts that one value, and anything else — includingus-east-1, which remains valid for every other resource — is rejected with400. Omit the field and you geteu-east-1. The region is only where the content is stored; serving is region-less, so it does not affect how the site is reached. - Copy security settings when cloning:
POST /api/v1/apps/{uuid}/cloneacceptsclone_security(boolean, defaultfalse), which copies allowed connections and Basic Auth from the source application. The flag was previously undocumented. - Outbound webhooks: register an endpoint and the platform POSTs deployment events to it, so you no longer have to poll
GET /api/v1/apps/{uuid}/deploymentsto find out when a deploy finished. Every endpoint below requires theworkspace:integrationspermission.GET /api/v1/webhooksandPOST /api/v1/webhooksto list and create.GET,PUTandDELETE /api/v1/webhooks/{uuid}to read, update and remove one.PUTchanges only the fields you send.POST /api/v1/webhooks/{uuid}/testto send a test event and get the result back immediately.GET /api/v1/webhooks/{uuid}/deliveriesfor delivery history.POST /api/v1/webhooks/{uuid}/deliveries/{delivery_uuid}/retryto replay a delivery.
- Two event types to subscribe to:
deploy_startedwhen a deployment entersrunning, anddeploy_endedwhen it reachescompleted,failedorcancelled. There is no separate build event — on Miget the build and the deployment are one lifecycle, andbuild_idis the deployment UUID. - Static site deployments emit these events too, with
data.app_idholding the UUID thatGET /api/v1/staticreturns — the same UUID that scopes a webhook to a single site. Aziporsftpdeployment has no commit behind it, socommit_sha,commit_messageandbrancharrive asnull. - Deliveries follow the Standard Webhooks specification, so any Standard Webhooks library verifies them as-is. Every request carries
webhook-id,webhook-timestampandwebhook-signature, where the signature is an HMAC-SHA256 of{webhook-id}.{webhook-timestamp}.{body}, base64-encoded and prefixedv1,. Reject deliveries whose timestamp is more than a few minutes old — that is what makes a captured request non-replayable. - The signing secret is returned exactly once.
POST /api/v1/webhooksis the only response containingsecret; every other endpoint omits it, and there is no rotation endpoint. Store it when you create the webhook — to replace it, delete the webhook and create a new one. - Scope a webhook to specific apps with
app_uuids. Leave it empty and the webhook covers every app in the workspace, including apps created later. The apps must belong to the same workspace or the request is rejected with422. - Delivery history tells you whether your endpoint is actually working. Each entry carries
status(pending,deliveredorfailed),response_code, amessageholding the response body or the transport error, anattemptscount, andpayload— the exact JSON body that was sent, so you can compare it against what you received. There is one entry per event, updated in place across retries, and the last 50 events per webhook are kept. - Test an endpoint before it matters.
POST /api/v1/webhooks/{uuid}/testPOSTs a signed event immediately and returns the resulting delivery, so you can verify a URL and your signature verification without waiting for a real deployment. It works on a disabled webhook, and responds200whether or not your endpoint accepted it — readstatuson the returned delivery. - Your consumer needs to tolerate a
pingtype. The test event carries"type": "ping"with an emptydataobject, andpingis not one of the subscribable event types. A consumer that rejects unknown types will fail the test even though the endpoint is otherwise fine. Acceptpingexplicitly, or ignore types you do not recognise. - Failed deliveries are retried for about 24 hours, across 9 attempts spaced 30s, 2m, 8m, 30m, 1h, 3h, 8h and 12h apart with jitter. Make your endpoint idempotent and deduplicate on the event
id, which is stable across automatic retries and across manual replays. A failing endpoint is never disabled automatically. - The endpoint must be publicly reachable. A
urlpointing into private address space — loopback, RFC1918, link-local including169.254.169.254, CGNAT, or thelocalhost,.localand.internalhostnames — is rejected with422. The host is re-resolved before every delivery, so a name that later starts resolving to a private address stops being delivered to rather than being retried.
Fixed
- Deploying an unresolvable commit:
POST /api/v1/apps/{uuid}/deploywith acommit_shathat is not in the GitHub repository the application is configured with now returns422and names the commit, instead of reporting201 Application is being deployedand failing in the build several seconds later. Push the commit before deploying it, and pass a SHA from that same repository — one that is unpushed, on a fork, or removed by a force-push will not resolve. This check applies to applications deployed from GitHub; deploying the latest commit on a branch already behaved this way. - Unknown
parent_app_id:POST /api/v1/appswithdeployment_method: "parent_image"and aparent_app_idthat does not exist now returns422saying so. It previously returned500.
Added
- Git build settings:
POST /api/v1/appsandPUT /api/v1/apps/{uuid}/deploymentnow acceptproject_path,run_command,language,build_command,pre_deploy_command,post_deploy_command, anduse_dhiin thepublic_gitandgithubdeployment configuration. The same fields are returned indeployment_configon the app response. - Release-phase commands:
pre_deploy_commandruns once before a new release starts andpost_deploy_commandonce after a successful deploy. Usepre_deploy_commandfor database migrations (npx prisma migrate deploy,alembic upgrade head,bin/rails db:migrate) so they run once per release rather than on every replica boot. - Custom builder configuration:
builder: "custom"requireslanguageandbuild_command, which the API previously rejected. You can now select and configure the custom builder in the same request. - Monorepo builds: set
project_pathto the subdirectory holding the app and the build treats it as the root. - App URL on the app response:
POST /api/v1/appsandGET /api/v1/apps/{uuid}returnpublic_url, the address the app is served on. Thenameyou send gets a random suffix appended, so the URL is never the one you would have built from that name — read it back from the response. Custom domains are not included; they are listed underGET /api/v1/apps/{uuid}/domains. - Plan type on the plan object:
GET /api/v1/plansreturnsplan_type(devorpro) on each plan, so the two tiers can be told apart from the plan list itself.
Changed
- Deployment updates are a patch, not a replacement:
PUT /api/v1/apps/{uuid}/deploymentmergesdeployment_config_attributesover the stored configuration. Fields you omit keep their value, and a field sent as an empty string is cleared. Previously every omitted field was wiped, so a partial update could silently clear settings such as the repository URL. The method’s identifying fields are the exception and must still be sent on every request:repository_urlforpublic_git,repositoryandcredential_idforgithub,image_urlandtagforcontainer_registry. Sending a differentdeployment_methodstill builds the configuration from scratch, so supply every field that method needs. - Add-on creation validates its required fields:
POST /api/v1/apps/{uuid}/addonsnow rejects an incomplete request with400.labelis required.postgres_version,mysql_version, andvalkey_versionare each required for their matchingtype.mount_pointandstorage_accessare required for a standalonestorageadd-on — passservice_idinstead and both are inherited from that service. - Service creation validates its required fields:
POST /api/v1/servicesnow returns400whenpostgres_versionis missing forservice_type: "postgres", ormount_pointis missing forservice_type: "shared_storage". - Database versions are checked against a list: an unsupported version is rejected with
400instead of being accepted. Accepted values arepostgres_version:18,17,16,15,14,13;mysql_version:8.2,8.0;valkey_version:7,7.2.8.4was previously listed as a MySQL version but was never supported. plan_typeis no longer required when creating a resource:POST /api/v1/resourcesresolves the plan fromplan_code_namealone.plan_typeis still accepted for backward compatibility but is ignored. Pass acode_namefromGET /api/v1/plansverbatim — the values are opaque identifiers such asmiget_hobby_0, and they differ between environments.
Fixed
- App detail for
public_gitapps:GET /api/v1/apps/{uuid}returns the repository underrepository_url, matching the field you use to create and update it. This request previously failed for apps using thepublic_gitdeployment method. - Deployment update response:
PUT /api/v1/apps/{uuid}/deploymentreturns the deployment configuration it just saved. It previously returned a nulldeployment_config, so you had to re-fetch the app to see your own change. - App create response:
POST /api/v1/appsreturns thedeployment_configit just saved instead ofnull, so you can read back your own configuration without a follow-up request. - Storage add-on on a service without storage:
POST /api/v1/apps/{uuid}/addonswithtype: "storage"and aservice_idpointing at a service that has no storage now returns422explaining the problem. It previously returned500. - Rate-limited Git host: when a
public_gitrepository cannot be read because the Git host is rate-limiting the request, the error now says so and asks you to retry. It previously reported that the repository or branch does not exist, which sent you looking for a problem with a URL that was correct. - Branch with no commits: pointing a
public_gitdeployment at an empty branch now fails with a message naming the branch, instead of an unhelpful server error.
Added
- App internal URL & auth state:
GET /api/v1/apps/{uuid}now returnsinternal_url(the<service>.<resource>.<region>.migetapp.internal:5000address for app-to-app and addon connections,nulluntil a compute resource is assigned) andbasic_auth_enabled(whether HTTP Basic Auth is enforced at the ingress; credentials are never returned). - Deployment commit metadata: deployment records from
GET /api/v1/apps/{uuid}/deploymentsnow includecommit_sha,commit_message, andbranchfor git-based deployment methods (nullotherwise), so you can confirm which commit is live. - Cron run logs:
GET /api/v1/apps/{uuid}/cronjobs/{id}/stream_logsstreams the cron job’s most recent run logs over SSE (returns404until the job has run at least once).
Changed
- Deploy while busy:
POST /api/v1/apps/{uuid}/deploynow returns409 Conflict(previously a generic422) when a deployment is already in progress, with a message pointing atGET /api/v1/apps/{uuid}/deploymentsto poll before retrying.
Changed
- Scaling profile
within_resources: this field onPUT /api/v1/apps/{uuid}/scaling_profilewas never active, and its description did not say so. Scaling is always limited to the resource’s allocation; scaling beyond it is not implemented. The field is still accepted and requests need no change — the reference now states that it is ignored.
Changed
- Public Git deployment field: For
public_gitapps, thedeployment_configfield isrepository_url(a full HTTPS URL), notrepository. The OpenAPI spec now declaresrepository_urlonPOST /api/v1/appsandPUT /api/v1/apps/{uuid}/deployment.repositoryremains thegithubfield (owner/repoformat). Clients that were sendingrepositoryforpublic_gitshould switch torepository_url. - Switching to Git deployment:
PUT /api/v1/apps/{uuid}/deploymentnow documents thepublic_gitandgithubconfiguration bodies (previously onlycontainer_registry,parent_image, andkamalwere described). - Container image reference: Clarified that
image_urlmust be provided without a scheme and without a tag (e.g.docker.io/library/nginx); put the tag in the separatetagfield (defaults tolatest). - Deployment method naming: Corrected the documented enum for
deployment_methodtocontainer_registry(the valuedocker_registrynever existed).
Added
Deploy Button Deep Links
“Deploy to Miget” buttons can now open the deploy wizard prefilled from a source. Supported query parameters:
- Stack:
/stacks/new?repo=<url-or-slug>&branch=<branch>&path=<compose-path> - App (public Git):
/apps/new?repo=<repo-url>&branch=<branch> - App (container image):
/apps/new?image=<registry/image:tag>
Added
Docker Compose Stacks API
Deploy a multi-service application from a single
compose.yaml in a Git repository. Analyze a repo first, then create a stack that provisions every detected app and managed service together:- Analyze:
POST /api/v1/stacks/analyze(detects services and required env vars; creates nothing) - Create:
POST /api/v1/stacks - List / Get:
GET /api/v1/stacks·GET /api/v1/stacks/{uuid} - Update:
PUT /api/v1/stacks/{uuid}(label,compose_path) - Deploy:
POST /api/v1/stacks/{uuid}/deploy - Deployment config:
PUT /api/v1/stacks/{uuid}/deployment(branch, auto-deploy, repository) - Deployment history:
GET /api/v1/stacks/{uuid}/deployments·GET /api/v1/stacks/{uuid}/deployments/{id} - Delete:
DELETE /api/v1/stacks/{uuid}(cascades to the stack’s apps and services)
Git Credentials API
Read-only access to the workspace Git credentials (GitHub App installs / tokens) used to clone private repositories. The returned
uuid is what you pass as credential_id when creating a stack or a github/public_git app. Tokens are never returned.- List:
GET /api/v1/git_credentials - Get:
GET /api/v1/git_credentials/{uuid}
Changed
- App response now includes region:
GET /api/v1/apps/{uuid}now returns a nestedregionobject (id,name,code) alongside the app’s other fields.
Added
App Activity API
A new endpoint for retrieving the activity feed of an application:
- Get App Activity:
GET /api/v1/apps/{uuid}/activity(supportspageandlimitquery parameters for pagination)
Container Registry Credentials API
A workspace-scoped CRUD surface for managing registry credentials used by Dockerfile and container deployments:
- Create:
POST /workspaces/{workspace_id}/container_registry_credentials - Update:
PUT /workspaces/{workspace_id}/container_registry_credentials/{uuid} - Delete:
DELETE /workspaces/{workspace_id}/container_registry_credentials/{uuid}
Changed
- App Creation:
POST /api/v1/appsnow requires theresource_idparameter to identify the target Resource plan. The previousregion_idparameter has been removed because it was misleading on a fixed-capacity model. - App Domains Path: The path parameter on domain-scoped endpoints has been renamed from
uuidtodomain_uuidto disambiguate it from the parent app’suuid. This affects:GET /api/v1/apps/{uuid}/domains/{domain_uuid}PUT /api/v1/apps/{uuid}/domains/{domain_uuid}DELETE /api/v1/apps/{uuid}/domains/{domain_uuid}
- Replica Endpoints:
POST /api/v1/apps/{id}/addons/create_replicaandPOST /api/v1/services/{id}/create_replicanow return the fullAddonandServiceentities (previously returned a genericMessage). The response now includes the new replica’suuidandname. - Cron Job Delete:
DELETEon cron job endpoints now returns aMessagebody alongside the204status for consistency with other destroy endpoints. - Cron Job Entity: Renamed
statetostatus(running,stopped) and added two new required fields:last_job_name(name of the most recent run) andlast_job_status(e.g.running,complete,failed). Clients reading thestatefield should switch tostatus. - Region Entity: Now exposes a numeric
idfield in addition tonameandcode. Theidis required. - Container Image Overrides: App and addon deployment configs now accept
commandandargsarrays to override the image’sENTRYPOINTandCMD. Useful for images like Keycloak that print help on a bare run.
Added
Buckets API (S3-Compatible Object Storage)
A complete set of endpoints for managing S3-compatible storage buckets:
- List Buckets:
GET /api/v1/buckets - Create Bucket:
POST /api/v1/buckets - Get Bucket:
GET /api/v1/buckets`/{uuid}` - Update Bucket:
PUT /api/v1/buckets`/{uuid}` - Delete Bucket:
DELETE /api/v1/buckets`/{uuid}` - Regenerate Credentials:
POST /api/v1/buckets`/{uuid}`/regenerate_credentials - Update Policy:
PUT /api/v1/buckets`/{uuid}`/update_policy - Update ACL:
PUT /api/v1/buckets`/{uuid}`/update_acl
Bucket Objects API
File and folder management within buckets:
- List Objects:
GET /api/v1/buckets`/{uuid}`/objects/list - Get Upload URL:
POST /api/v1/buckets`/{uuid}`/objects/upload_url - Get Download URL:
POST /api/v1/buckets`/{uuid}`/objects/download_url - Create Folder:
POST /api/v1/buckets`/{uuid}`/objects/create_folder - Rename Object:
PUT /api/v1/buckets`/{uuid}`/objects/rename - Delete Object:
DELETE /api/v1/buckets`/{uuid}`/objects/destroy
Database Replication API
New endpoints for managing PostgreSQL streaming replication on both app addons and standalone services:
- Create Replica:
POST /api/v1/apps`/{id}`/addons/create_replicaandPOST /api/v1/services`/{id}`/create_replica - Promote Replica:
POST /api/v1/apps`/{id}`/addons/promote_replicaandPOST /api/v1/services`/{id}`/promote_replica - Promote External:
POST /api/v1/apps`/{id}`/addons/promote_externalandPOST /api/v1/services`/{id}`/promote_external - Enable Streaming Replication:
POST /api/v1/services`/{id}`/enable_streaming_replication - Disable Streaming Replication:
POST /api/v1/services`/{id}`/disable_streaming_replication
Changed
- Deploy Endpoint: The
POST /api/v1/apps`/{uuid}`/deployendpoint now supports deploying from GitHub repositories withgithub_urlandgithub_branchparameters, in addition to the existingcommit_shasupport.
Added
App Deployments API
A new set of endpoints for managing the application deployment lifecycle has been introduced:
- List Deployments:
GET /api/v1/apps`/{uuid}`/deployments - Get Deployment Details: “GET /api/v1/apps
/{uuid}/deployments/{id} - Get Build Logs:
GET /api/v1/apps`/{uuid}`/deployments`/{id}`/logs - Stream Build Logs:
GET /api/v1/apps`/{uuid}`/deployments`/{id}`/stream_logs - Cancel Deployment:
POST /api/v1/apps`/{uuid}`/deployments`/{id}`/cancel - Rollback Deployment:
POST /api/v1/apps`/{uuid}`/deployments`/{id}`/rollback
App Ports API
Full management of application network ports is now available:
- List Ports:
GET /api/v1/workspaces`/{ws}`/apps`/{app}`/ports - Create Port:
POST /api/v1/workspaces`/{ws}`/apps`/{app}`/ports - Delete Port: “DELETE /api/v1/workspaces
/{ws}/apps/{app}/ports/{id} - Expose Port Publicly:
PATCH /api/v1/workspaces`/{ws}`/apps`/{app}`/ports`/{id}`/expose_publicly - Make Port Private:
PATCH /api/v1/workspaces`/{ws}`/apps`/{app}`/ports`/{id}`/make_private
Add-on Password Rotation
The new
POST /api/v1/apps`/{uuid}`/addons`/{id}`/rotate_password endpoint allows for secure password rotation for supported add-ons, such as databases.Basic Authentication
The
PUT /api/v1/apps`/{uuid}`/security endpoint now supports basic_auth_enabled, basic_auth_username, and basic_auth_password parameters, allowing you to enable and configure Basic Authentication for your application.Changed
- Deployment Options: The
POST /api/v1/apps`/{uuid}`/deployendpoint now acceptscommit_shaandbranchparameters, enabling more precise deployments from Git repositories. - Builder Options: The
builderparameter inPOST /api/v1/appshas been expanded to supportauto(automatic buildpack detection) andcustom(language-specific builder) in addition todockerfile. - Resource Validation: The “PUT /api/v1/resources
/{uuid}endpoint now validates against plan downgrades to prevent assigning a plan with insufficient resources (CPU, RAM, storage). - Add-on Deletion Logic: The logic for deleting an add-on (“DELETE /api/v1/apps
/{uuid}/addons/{id}) has been refactored into a dedicated service object for improved maintainability.
Removed
- The
POST /api/v1/apps`/{uuid}`/sync_parent_imageendpoint has been removed.

