Changed

  • GET /api/v1/services/{id}/backups and GET /api/v1/apps/{uuid}/addons/{id}/backups separate the setting from the cluster. backup_enabled used to report what the cluster runs, which trails an update, so backups you had just enabled read back as false. It now reports the configured setting and changes as soon as PUT accepts it. The new backup_active field carries what the cluster runs. backup_enabled: true with backup_active: false means the change is still being applied. backup_enabled is always false on the free plan.
  • backup_enabled, public_access, s3_enabled and to_new_cluster are booleans. On POST and PUT /api/v1/services, POST and PUT /api/v1/apps/{uuid}/addons and both restore_backup endpoints they accept JSON true and false. backup_enabled, public_access and s3_enabled used to refuse them with 400. '1' and '0' are still accepted.

Fixed

  • to_new_cluster: true on restore_backup restores into a new cluster. Only the string '1' did. true was accepted and treated as false, 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}/clone answered to apps:deploy, apps:operate or apps:manage, as the 2026-08-29 entry below records. Creating an application already takes apps:manage, and the dashboard only ever offered cloning to it, so a role or token without it now gets 403.
  • A preview environment config must run on a resource that accepts its project. While enabled is true, PUT /api/v1/apps/{uuid}/preview_environments/config refuses a resource assigned to projects other than the target with 422. That applies to resource_id, and to the application’s own resource in parent mode.

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 state can read sleeping. A free-plan application that has had no traffic for 30 minutes is scaled to zero, and GET /api/v1/apps/{uuid} reports sleeping until something wakes it. The request that wakes it is answered with 503 while the application starts, which is a wake-up in progress rather than a failure. PATCH /api/v1/apps/{uuid}/state accepts schedule_start and schedule_stop on a sleeping application.

Fixed

  • A free application waking on traffic no longer sends app_state_changed. It arrived as started with 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 answered 403 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) and PUT /api/v1/vpcs/{uuid} moved from network:manage to network:operate. They cost nothing and are undone by the role that made them, which is what network:operate is for. Creating and deleting a VPC, and switching a VPN gateway on or off, stay on network:manage.
  • Site-to-site tunnels are no longer restricted to the workspace owner: POST and DELETE /api/v1/vpcs/{uuid}/site_connections now take network:manage. The address pool and the $199/mo charge are unchanged, and a region with no free address still answers 422.
  • Application ports. POST, DELETE /api/v1/apps/{uuid}/ports and the expose_publicly / make_private actions moved from apps:manage to apps: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. GET and PUT /api/v1/apps/{uuid}/preview_environments/config moved from apps:manage to apps:operate; POST .../preview_environments/{uuid}/redeploy moved to apps:deploy, which is what a rollback and a cancel already take. Deleting a preview environment stays on apps:manage.
  • PUT /api/v1/static_sites/{uuid} moved from apps:manage to apps: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}/security and PUT /api/v1/apps/{uuid}/deployment take apps:deploy, apps:operate or apps:manage; /vars and /secret_files take apps:operate or apps:manage; PUT /api/v1/static_sites/{uuid}/deployment takes apps:operate. All of them previously required apps:manage.
  • Public access is operate in both directions. public_access on an add-on or on PUT /api/v1/services/{id}, and a port created with public: true, answer to apps:operate / services:operate like 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} and POST .../reset_database did not move down with the rest of the add-on endpoints, and POST /api/v1/services/{id}/reset_database moved up to services:manage: neither the add-on nor the rows in it come back. POST .../restore_backup stays on operate — 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}/addons and PATCH .../addons/{id}/state, along with every /cronjobs endpoint, moved down from apps: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 already apps:operate.
  • Reading a cron job takes apps:view. GET /api/v1/apps/{uuid}/cronjobs, GET .../cronjobs/{id} and its log stream answered apps:operate, which was stricter than the dashboard and stricter than every other read. Creating, changing and deleting one stays on apps:operate.
  • Deleting a project no longer requires having created it. DELETE /api/v1/projects/{uuid} takes projects:manage, so a workspace owner can remove a project somebody else made — the dashboard never asked for more. /vars and /secret_files on a project now accept projects:manage as well as projects:operate.
  • Buckets now answer the same permission here as they do in the dashboard. PUT /api/v1/buckets/{uuid} moved from buckets:manage to buckets:operate — the disk allocation included — because a bucket’s settings are operate everywhere else. POST /api/v1/buckets/{uuid}/objects/download_url is unchanged at buckets:view; it was the dashboard that asked for more, and it no longer does. Uploading, renaming and deleting objects stay on buckets:operate.

Fixed

  • A public port answers with its address instead of 500. PATCH /api/v1/apps/{uuid}/ports/{port_id}/expose_publicly and POST /api/v1/apps/{uuid}/ports with public: true failed outright: the response described the proxy the port landed on as a nested port_endpoint object, 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 — alongside public_endpoint (hostname:port) and external_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/apps and GET /api/v1/apps/{uuid}/activity take apps:view; POST /api/v1/apps/{uuid}/deploy and PATCH /api/v1/apps/{uuid}/state take apps:deploy; POST /api/v1/apps/{uuid}/clone, PUT /api/v1/apps/{uuid}/health_checks and PUT /api/v1/apps/{uuid}/scaling_profile take apps:deploy, apps:operate or apps:manage. Until now any workspace member reached them, and a workspace token limited to :view permissions could deploy a commit, stop an application and raise its replica count. Those calls answer 403 from 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_blocked and scaling_limit_reached carry data.resource_name and data.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_ended carries data.message, the reason a deployment failed, in the platform’s own wording where it recognises the underlying error. It is null on a deployment that did not fail.
  • app_state_changed reports a requested restart. data.state is one of started, stopped, failed or restart_scheduled. A restart now delivers two events rather than none: restart_scheduled when it is asked for, and started once the application is running again. Subscribers filtering on state should 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 is network:view, attach and detach are network:operate, everything else is network:manage.
    • GET /api/v1/vpcs and POST /api/v1/vpcs to list and create. region_id is required; name defaults to default and cidr_v4 to 10.224.0.0/16. A range that is not RFC1918, is smaller than /22, or overlaps a platform network is refused with 422 naming the conflict. A VPC belongs to one region and cannot span regions.
    • GET /api/v1/vpcs/{uuid} returns the network including resolver_v4, the VPC’s own DNS resolver, and vpn_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 with 422 while any subnet remains.
    • GET, POST /api/v1/vpcs/{uuid}/subnets and DELETE /api/v1/vpcs/{uuid}/subnets/{subnet_uuid} manage the ranges carved out of the VPC. family is dual by default, or ipv4 for no IPv6 at all and ipv6 for no IPv4; cidr_v4 is required unless the family is ipv6, and cidr_v6 is derived from the VPC’s own when omitted. public defaults to true and is the only thing that distinguishes two subnets: public keeps the platform default route, private moves it to the VPC. Deleting is refused with 422 while a workload is still attached.
    • GET, POST /api/v1/vpcs/{uuid}/attachments and DELETE /api/v1/vpcs/{uuid}/attachments/{attachment_uuid} attach and detach workloads. Attaching takes subnet_uuid, attachable_type (App, Service or Addon) and attachable_uuid, and is refused with 422 when the workload runs in a different region from the VPC.
  • vpc_uuid and vpc_subnet_uuid on POST /api/v1/apps: join a VPC when the application is created instead of attaching afterwards, which avoids the restart an attach costs. vpc_subnet_uuid implies its VPC, so vpc_uuid can be omitted. The VPC must be in the same region as resource_id.
  • sub_path on shared storage: accepted on POST /api/v1/apps/{uuid}/addons for the storage type and on POST /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_gateways and DELETE /api/v1/vpcs/{uuid}/vpn_gateways/{gateway_uuid} enable a terminator. kind is wireguard, tailscale or cloudflare, one of each per VPC. Cloudflare needs organization, client_id and client_secret; Tailscale needs auth_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 with 422 while site-to-site connections still terminate on the gateway.
    • GET, POST /api/v1/vpcs/{uuid}/vpn_users and DELETE /api/v1/vpcs/{uuid}/vpn_users/{user_uuid} manage WireGuard devices. POST requires WireGuard to be enabled first, or answers 422.
    • GET /api/v1/vpcs/{uuid}/vpn_users/{user_uuid}/config returns 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 answers 410, and so does a call made before the device is active. 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/apply and DELETE /api/v1/vpcs/{uuid}/peer_routes/{route_uuid} stage and apply the ranges reachable behind a terminator. gateway_kind takes 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. apply answers 422 when nothing is staged.
    • GET, POST /api/v1/vpcs/{uuid}/site_connections and DELETE /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 gets 403 — and a region with no free address answers 422 rather than queueing. Read public_ip off the response: that is the address to configure on the far side. The pre-shared keys sent in tunnels are stored encrypted and never returned.
  • PUT /api/v1/vpcs/{uuid} changes a VPC’s label and is_default. The name and the address ranges are fixed at creation, because the platform derives the whole address plan from them on every action. Setting is_default while another VPC in that region holds it is refused with 422 rather than moving it aside.

Changed

  • A new VPC answers pending. POST /api/v1/vpcs returns before the platform has derived cidr_v6, resolver_v4 and vpn_pool_v4, so all three come back null. Poll GET /api/v1/vpcs/{uuid} until status is active, then read them.
  • A new VPC already has a subnet. The platform creates one named default, a /24 taken after the block it keeps for the router, the resolver and the VPN terminators — 10.224.1.0/24 on a 10.224.0.0/16 VPC. List GET /api/v1/vpcs/{uuid}/subnets before creating one — attaching a workload usually needs no subnet call at all, and carving 10.224.1.0/24 by hand is refused with 422 for 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 fqdn is 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 no fqdn at all.
  • POST /api/v1/apps/{uuid}/cronjobs requires label and command, plus cron when schedule_type is "cron". schedule_type now defaults to "interval". Previously name was the only required field while label and command were documented as optional — omitting either failed the call, and a name sent with the request is discarded, since the identifier the job runs under is generated and returned as name.
  • visibility on POST /api/v1/buckets and PUT /api/v1/buckets/{uuid} is validated against private_access and public_access. Any other value is now rejected with 400 before it reaches the bucket, instead of 422 carrying 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_app used to create a second storage addon with its own mount point, and unmount_app then 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}/apps and every response carrying an add-on answered 500 for the whole workspace while any add-on existed without its configuration row — the window between creating a database and its credentials being written. connection_details now 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_publicly and PATCH .../make_private answered 500 for every ordinary refusal — creating a port that already exists, or exposing one in a region with no free endpoint. They now answer 422 with the reason in errors.
  • A malformed SSH public key is rejected, not a 500. POST /api/v1/users/me/ssh_keys answered 500 when the key was the right shape but the wrong bytes — a truncated paste, or key material of the wrong length. It now answers 422 naming the problem.
  • Bucket object keys with a file extension are addressable again. DELETE /api/v1/buckets/{uuid}/objects/{key} and PUT /api/v1/buckets/{uuid}/objects/{key}/rename read the extension as a response-format suffix and answered with a page-not-found for any real filename, docs%2Freport.pdf included. Keys with no dot were unaffected.

Added

  • app_state_changed webhook event: fires when somebody starts, stops, or restarts an application. app_stopped does not cover this — that one fires when the platform stops an application after repeated crashes, and the two stay separate events.
  • include_review_apps on webhooks: accepted on POST /api/v1/webhooks and PUT /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 with app_uuids receives nothing from the preview environments of those applications. Set it to true to have their events matched against the parent as well. Defaults to false, 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/resources returns the usual checkout_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 no components, 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 422 without 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_5 with 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}/resources with resource_id assigns one. Needs both projects:manage and resources:manage.
    • DELETE /api/v1/projects/{project_id}/resources/{resource_id} returns it to the shared pool. Needs only projects:manage — removing an assignment only ever widens access, and every workload already there stays legal.
    • Assigning is refused with 422 while 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. Send with_hosting_projects: true to 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) and project_ids (project UUIDs). assigned: false means any project may deploy on it. Read both before offering a resource_id: the resource is usable when assigned is false, or when project_ids contains the target project. project_ids lists only projects you can access.
  • Project access: a project can be closed to a list of members and roles, returned as restrictions on the project response. Empty means the project is open to the whole workspace.
    • POST /api/v1/projects/{project_id}/restrictions with exactly one of user_email or role_name; sending both or neither is a 400. Adding the first entry is what closes the project.
    • DELETE /api/v1/projects/{project_id}/restrictions/{id} removes one entry, where {id} is the numeric id from the restrictions list. 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_id on PUT /api/v1/apps/{uuid}, PUT /api/v1/static/{uuid}, PUT /api/v1/services/{id}, PUT /api/v1/stacks/{uuid} and PUT /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_id is accepted on POST /api/v1/buckets and PUT /api/v1/buckets/{uuid}. It is never inferred — omit it and the bucket has no project, even in a workspace with exactly one. Send null on 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-Id naming a different one is refused with 403), cannot reach /api/v1/users/me or 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, not workspace: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 returns 404, never 403. 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_id you cannot reach and one that does not exist answer identically: 404 with {"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} and PUT /api/v1/resources/{uuid}/labels previously allowed the user who created the resource through regardless of role; they now require resources:operate like every other caller.

Fixed

  • The permission reference was wrong in both directions. It listed apps:create, resource:view, resource:manage and workspace:settings, none of which exist, and omitted every services: permission along with projects:operate and five of the seven workspace: ones. A 403 can 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/static and POST /api/v1/static to list and create.
    • GET, PUT and DELETE /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}/deployment to update build and routing settings. Applied as a patch: fields you omit keep their stored value. source_type is not accepted here.
    • POST /api/v1/static/{uuid}/deployments to deploy — send a multipart archive (zip, max 1 GB) for a zip site, or omit it to rebuild a github site. Returns 409 Conflict while a deployment is in flight.
    • GET /api/v1/static/{uuid}/deployments for deployment history.
    • POST /api/v1/static/{uuid}/deployments/{id}/cancel to stop a build that is still running. Only git_push and github sites run a build; a zip or sftp deployment is a direct sync with nothing to interrupt.
    • POST /api/v1/static/{uuid}/deployments/{id}/rollback to republish the site from the commit that deployment shipped. github only — a static site produces no image, so there is no stored artifact to redeploy and the platform rebuilds that commit instead. A git_push site is rolled back by pushing again, and zip and sftp deployments carry no commit; both return 422.
    • GET /api/v1/static/{uuid}/files to browse deployed content one directory level per call (prefix, limit, cursor). Read-only. Returns 422 while the site’s storage is still being provisioned.
    • GET|POST /api/v1/static/{uuid}/domains, PUT|DELETE .../domains/{domain_uuid} and POST .../domains/{domain_uuid}/verify for custom domains, in the same shape as an app’s.
  • Static site content sources: deployment_config.source_type is required at creation and is one of github, git_push, zip or sftp. It decides what gets provisioned and cannot be changed afterwards — switching means creating another site. github and git_push are built for you with the generator auto-detected; build_command, output_dir and project_path override detection, and spa_mode rewrites unknown paths to /index.html. zip and sftp take already-built output.
  • Connection details come back ready to use: deployment_config.git_ssh_url is the remote a git_push site is pushed to, and appears once its repository has been provisioned — poll for it before pushing. deployment_config.sftp_endpoint is the complete user@host target for an sftp site. 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 422 rather than silently renamed.
  • Static sites are created in eu-east-1 only: region on POST /api/v1/static accepts that one value, and anything else — including us-east-1, which remains valid for every other resource — is rejected with 400. Omit the field and you get eu-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}/clone accepts clone_security (boolean, default false), 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}/deployments to find out when a deploy finished. Every endpoint below requires the workspace:integrations permission.
    • GET /api/v1/webhooks and POST /api/v1/webhooks to list and create.
    • GET, PUT and DELETE /api/v1/webhooks/{uuid} to read, update and remove one. PUT changes only the fields you send.
    • POST /api/v1/webhooks/{uuid}/test to send a test event and get the result back immediately.
    • GET /api/v1/webhooks/{uuid}/deliveries for delivery history.
    • POST /api/v1/webhooks/{uuid}/deliveries/{delivery_uuid}/retry to replay a delivery.
  • Two event types to subscribe to: deploy_started when a deployment enters running, and deploy_ended when it reaches completed, failed or cancelled. There is no separate build event — on Miget the build and the deployment are one lifecycle, and build_id is the deployment UUID.
  • Static site deployments emit these events too, with data.app_id holding the UUID that GET /api/v1/static returns — the same UUID that scopes a webhook to a single site. A zip or sftp deployment has no commit behind it, so commit_sha, commit_message and branch arrive as null.
  • Deliveries follow the Standard Webhooks specification, so any Standard Webhooks library verifies them as-is. Every request carries webhook-id, webhook-timestamp and webhook-signature, where the signature is an HMAC-SHA256 of {webhook-id}.{webhook-timestamp}.{body}, base64-encoded and prefixed v1,. 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/webhooks is the only response containing secret; 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 with 422.
  • Delivery history tells you whether your endpoint is actually working. Each entry carries status (pending, delivered or failed), response_code, a message holding the response body or the transport error, an attempts count, and payload — 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}/test POSTs 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 responds 200 whether or not your endpoint accepted it — read status on the returned delivery.
  • Your consumer needs to tolerate a ping type. The test event carries "type": "ping" with an empty data object, and ping is not one of the subscribable event types. A consumer that rejects unknown types will fail the test even though the endpoint is otherwise fine. Accept ping explicitly, 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 url pointing into private address space — loopback, RFC1918, link-local including 169.254.169.254, CGNAT, or the localhost, .local and .internal hostnames — is rejected with 422. 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}/deploy with a commit_sha that is not in the GitHub repository the application is configured with now returns 422 and names the commit, instead of reporting 201 Application is being deployed and 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/apps with deployment_method: "parent_image" and a parent_app_id that does not exist now returns 422 saying so. It previously returned 500.

Added

  • Git build settings: POST /api/v1/apps and PUT /api/v1/apps/{uuid}/deployment now accept project_path, run_command, language, build_command, pre_deploy_command, post_deploy_command, and use_dhi in the public_git and github deployment configuration. The same fields are returned in deployment_config on the app response.
  • Release-phase commands: pre_deploy_command runs once before a new release starts and post_deploy_command once after a successful deploy. Use pre_deploy_command for 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" requires language and build_command, which the API previously rejected. You can now select and configure the custom builder in the same request.
  • Monorepo builds: set project_path to the subdirectory holding the app and the build treats it as the root.
  • App URL on the app response: POST /api/v1/apps and GET /api/v1/apps/{uuid} return public_url, the address the app is served on. The name you 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 under GET /api/v1/apps/{uuid}/domains.
  • Plan type on the plan object: GET /api/v1/plans returns plan_type (dev or pro) 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}/deployment merges deployment_config_attributes over 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_url for public_git, repository and credential_id for github, image_url and tag for container_registry. Sending a different deployment_method still builds the configuration from scratch, so supply every field that method needs.
  • Add-on creation validates its required fields: POST /api/v1/apps/{uuid}/addons now rejects an incomplete request with 400. label is required. postgres_version, mysql_version, and valkey_version are each required for their matching type. mount_point and storage_access are required for a standalone storage add-on — pass service_id instead and both are inherited from that service.
  • Service creation validates its required fields: POST /api/v1/services now returns 400 when postgres_version is missing for service_type: "postgres", or mount_point is missing for service_type: "shared_storage".
  • Database versions are checked against a list: an unsupported version is rejected with 400 instead of being accepted. Accepted values are postgres_version: 18, 17, 16, 15, 14, 13; mysql_version: 8.2, 8.0; valkey_version: 7, 7.2. 8.4 was previously listed as a MySQL version but was never supported.
  • plan_type is no longer required when creating a resource: POST /api/v1/resources resolves the plan from plan_code_name alone. plan_type is still accepted for backward compatibility but is ignored. Pass a code_name from GET /api/v1/plans verbatim — the values are opaque identifiers such as miget_hobby_0, and they differ between environments.

Fixed

  • App detail for public_git apps: GET /api/v1/apps/{uuid} returns the repository under repository_url, matching the field you use to create and update it. This request previously failed for apps using the public_git deployment method.
  • Deployment update response: PUT /api/v1/apps/{uuid}/deployment returns the deployment configuration it just saved. It previously returned a null deployment_config, so you had to re-fetch the app to see your own change.
  • App create response: POST /api/v1/apps returns the deployment_config it just saved instead of null, 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}/addons with type: "storage" and a service_id pointing at a service that has no storage now returns 422 explaining the problem. It previously returned 500.
  • Rate-limited Git host: when a public_git repository 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_git deployment 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 returns internal_url (the <service>.<resource>.<region>.migetapp.internal:5000 address for app-to-app and addon connections, null until a compute resource is assigned) and basic_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}/deployments now include commit_sha, commit_message, and branch for git-based deployment methods (null otherwise), so you can confirm which commit is live.
  • Cron run logs: GET /api/v1/apps/{uuid}/cronjobs/{id}/stream_logs streams the cron job’s most recent run logs over SSE (returns 404 until the job has run at least once).

Changed

  • Deploy while busy: POST /api/v1/apps/{uuid}/deploy now returns 409 Conflict (previously a generic 422) when a deployment is already in progress, with a message pointing at GET /api/v1/apps/{uuid}/deployments to poll before retrying.

Changed

  • Scaling profile within_resources: this field on PUT /api/v1/apps/{uuid}/scaling_profile was 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_git apps, the deployment_config field is repository_url (a full HTTPS URL), not repository. The OpenAPI spec now declares repository_url on POST /api/v1/apps and PUT /api/v1/apps/{uuid}/deployment. repository remains the github field (owner/repo format). Clients that were sending repository for public_git should switch to repository_url.
  • Switching to Git deployment: PUT /api/v1/apps/{uuid}/deployment now documents the public_git and github configuration bodies (previously only container_registry, parent_image, and kamal were described).
  • Container image reference: Clarified that image_url must be provided without a scheme and without a tag (e.g. docker.io/library/nginx); put the tag in the separate tag field (defaults to latest).
  • Deployment method naming: Corrected the documented enum for deployment_method to container_registry (the value docker_registry never 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 nested region object (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 (supports page and limit query 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/apps now requires the resource_id parameter to identify the target Resource plan. The previous region_id parameter 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 uuid to domain_uuid to disambiguate it from the parent app’s uuid. 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_replica and POST /api/v1/services/{id}/create_replica now return the full Addon and Service entities (previously returned a generic Message). The response now includes the new replica’s uuid and name.
  • Cron Job Delete: DELETE on cron job endpoints now returns a Message body alongside the 204 status for consistency with other destroy endpoints.
  • Cron Job Entity: Renamed state to status (running, stopped) and added two new required fields: last_job_name (name of the most recent run) and last_job_status (e.g. running, complete, failed). Clients reading the state field should switch to status.
  • Region Entity: Now exposes a numeric id field in addition to name and code. The id is required.
  • Container Image Overrides: App and addon deployment configs now accept command and args arrays to override the image’s ENTRYPOINT and CMD. 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_replica and POST /api/v1/services`/{id}`/create_replica
  • Promote Replica: POST /api/v1/apps`/{id}`/addons/promote_replica and POST /api/v1/services`/{id}`/promote_replica
  • Promote External: POST /api/v1/apps`/{id}`/addons/promote_external and POST /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}`/deploy endpoint now supports deploying from GitHub repositories with github_url and github_branch parameters, in addition to the existing commit_sha support.

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}`/deploy endpoint now accepts commit_sha and branch parameters, enabling more precise deployments from Git repositories.
  • Builder Options: The builder parameter in POST /api/v1/apps has been expanded to support auto (automatic buildpack detection) and custom (language-specific builder) in addition to dockerfile.
  • 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_image endpoint has been removed.