Add service
POSThttps://api.instacloud.com/projects/{projectId}/services
Token scope: account · org · project — read-only tokens refused (not a GET).
Add a service on demand. Assigns a default access domain (postgres/compute). Gated — service.add.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
projectId | path | string | yes |
Request body
application/json
| Field | Type | Required | Description |
|---|---|---|---|
type | "postgres" | "storage" | "compute" | "redis" | "mysql" | "mongodb" | yes | |
name | string | yes | lower-kebab: a-z, 0-9, hyphen |
branch | string | no | target branch (default: project default branch) |
region | "us-east" | "eu-central" | "ap-southeast" | no | region for postgres/compute/managed databases (see GET /regions); invalid for storage |
public | boolean | no | storage only — provision the bucket with anonymous public-read (default false) |
image | string | no | compute only — container image to run at creation (e.g. nginx:1.27) |
port | integer | no | compute only — port the image listens on (default 8080) |
endpoint | "http" | "tcp" | "udp" | "tcp+udp" | no | compute only — how the service is reached. http (default): HTTPS at the service domain. tcp / udp / tcp+udp: raw TCP, UDP, or both on `port` at the service’s own public IPv6 address (IPv6 clients only, no platform TLS, no custom domain). These need an explicit port. tcp refuses ports that are commonly scanned (21, 22, 23, 25, 53, 445, 3389), web ports (80, 443) or reserved by the platform; udp refuses known amplification ports (7, 17, 19, 53, 111, 123, 137, 138, 161, 162, 389, 520, 1900, 3702, 5353, 11211) and ports reserved by the platform; tcp+udp refuses both lists. Available on the insta-compute provider only. |
alwaysOn | boolean | no | compute and databases (postgres, redis, mysql, mongodb) — idle mode at creation; not for storage. Omitted: the platform default per kind. Compute: INSTA_COMPUTE_ALWAYS_ON_DEFAULT (false on InstaCloud: scale-to-zero — a machine with no inbound or outbound traffic suspends and wakes on the next request). Databases: INSTA_DB_ALWAYS_ON_DEFAULT (true on InstaCloud: the database stays warm, so an app waking from scale-to-zero never waits on a database cold start too; false opts a new database into scale-to-zero — the idle instance suspends and its first connection after idling cold-starts). true = always-on: never scales to zero. A worker (port 0) is always-on regardless; false with port 0 is refused. For postgres the value is recorded on the database instance (GET /projects/{projectId}/database/instance, scaleToZero) and changed with PATCH .../database/settings; for the other types on the service row (always_on) and changed with PUT .../services/{serviceId}/always-on. All plans; billing is actual usage either way — an always-on database bills its resident memory while idle. |
volumeMountPath | string | no | Compute only: container mount path chosen on attachment; defaults to /data. Requires volumeGib only when attaching. Existing path changes are staged until the next deploy. |
volumeGib | integer | no | compute only — attach a persistent volume of this many whole Gi (attachable later too via PUT /services/:id/volume; the volume never detaches, but DELETE /services/:id/volume destroys it, data and all). Any plan may create up to the configured free volume cap (10Gi by default, env-overridable per deployment); a larger size is paid and capped at the org's own plan cap (50Gi on paid plans by default), like growing it later. Constraints: machineCount stays 1, and scale-to-zero uses stop (cold wake) instead of suspend — both lift if the volume is deleted. |
pgVersion | integer | no | postgres only — the Postgres major version to create the database on: 16, 17, 18. Omitted: the platform default (INSTA_DB_POSTGRES_MAJOR; 16 on InstaCloud). Fixed for the database's lifetime; branches keep it. 501 when the database provider cannot create that major yet. |
source | object | no | |
source.type | "github" | no | |
source.owner | string | yes | |
source.repo | string | yes | |
source.installationId | integer | no | GitHub App installation id; required unless public |
source.repoId | integer | no | GitHub repository id; required unless public |
source.public | boolean | no | a public repo, connected without an App installation: one-shot deploys, auto_deploy is always false |
source.branch | string | no | the repo branch to track; default: the repo’s default branch |
source.rootDir | string | null | no | subdirectory to build from (monorepo); relative, no leading / or ..; null/omitted = repo root. It also decides which pushes deploy this service: the build context is this directory, so a push that changed nothing under it is skipped |
source.autoDeploy | boolean | no | rebuild on every push to `branch` (default true; must be false or omitted for a public repo) |
source.buildCommand | string | null | no | |
source.startCommand | string | null | no | |
source.watchPaths | string[] | null | no | An include list of gitignore patterns, matched against paths RELATIVE TO THE REPOSITORY ROOT rather than to `rootDir` — e.g. ["apps/web/**", "packages/ui/**"]. A push deploys this service only when it changed a matching path; omit or null to fall back to the root-directory rule (a service with `rootDir` deploys when a push changed something under it, one building from the repo root deploys on every push). `!` narrows the list under git's own rule that a path cannot be re-included once an earlier pattern took its directory, so ["apps/web/**", "!**/*.md"] drops apps/web/README.md but not apps/web/docs/guide.md. Inert on a public repo, which cannot auto-deploy at all |
source.port | integer | null | no | the port the app listens on; written to the service |
Responses
201
Default Response
| Field | Type | Required | Description |
|---|---|---|---|
service | object | no | A project-level service (postgres | storage | compute | redis | mysql | mongodb). |
service.id | string | no | |
service.project_id | string | no | |
service.type | "postgres" | "storage" | "compute" | "redis" | "mysql" | "mongodb" | no | |
service.name | string | no | |
service.spec | object | no | compute: {cpu_kind,cpus,memory_mb}; postgres: sized by its instance ceiling (cpuMilli/memoryMib) |
service.machine_count | integer | no | compute only |
service.desired_state | "running" | "stopped" | "suspended" | no | compute only — developer-set lifecycle intent |
service.always_on | boolean | no | compute and managed databases (redis, mysql, mongodb) — true: machines never scale to zero; false: idle machines suspend and wake on request. A new compute service is born false (scale-to-zero, except a worker); a new managed database is born from INSTA_DB_ALWAYS_ON_DEFAULT (true on InstaCloud). Postgres is not recorded here: its idle mode lives on the database instance (GET /projects/{projectId}/database/instance, scaleToZero). Billing is actual usage either way. |
service.websocket | boolean | null | no | compute only — the app speaks WebSocket, so its machines carry connections-based concurrency and a 512 MB guest floor. Re-asserted by any redeploy given no flag, including restart. null: never recorded. |
service.endpoint | "http" | "tcp" | "udp" | "tcp+udp" | null | no | compute only — how the service is reached. http: Host-routed HTTPS at `domain`. tcp / udp / tcp+udp: raw TCP, UDP, or both at `endpoint_host`:`port` on the service’s own public IPv6 address (IPv6 clients only; no TLS from the platform, no custom domain, no WebSocket flag). Chosen at create, and changed by a deploy that passes `endpoint`. null: never chosen, which is http. |
service.endpoint_host | string | null | no | compute, endpoint tcp/udp/tcp+udp only — the host to reach on `port` (an IPv6-only name), set once a deploy has confirmed it; null otherwise |
service.region | string | null | no | |
service.domain | string | null | no | default access domain (postgres/compute) |
service.public | boolean | no | storage — bucket(s) served with anonymous public-read; managed databases — the database port is exposed on a public address (on insta-compute, false = reachable only over the private network, *_PRIVATE_URL); postgres — the public endpoint accepts connections (true by default; false = not reachable from the internet, private network lane only); compute — the service answers the internet at its URL / endpoint_host (true by default; false = the URL answers 403 and never wakes it, and only services in the same branch reach it, at private_host). Change it with PUT .../services/{serviceId}/access |
service.private_host | string | null | no | compute and managed databases on insta-compute — the name other services in the SAME project branch reach this one at, on `port` over TCP: `<name>.insta.internal` for compute, `<name>-<type>.insta.internal` for a managed database (redis/mysql/mongodb). Other branches and projects cannot reach it. Resolves through the search domain every service is started with; tools that skip the search list (nslookup, dig) need private_fqdn. null: the service has no private address (a worker, a name ending in '-', a service not yet back-filled, or a region without the private network) |
service.private_fqdn | string | null | no | compute and managed databases on insta-compute — the fully-qualified form of private_host, `<private name>.<network>.insta.internal`, for clients that do not use the search domain. null whenever private_host is |
service.image | string | null | no | compute only — container image currently run |
service.port | integer | null | no | compute/managed database — compute image port, or the private database TCP port |
service.start_command | string | null | no | Runtime startup override, run via sh -c; on insta-compute it runs in place of the image's ENTRYPOINT and CMD: a wrapper entrypoint (e.g. docker-entrypoint.sh) does not run while it is set. null restores the image's own ENTRYPOINT and CMD. Stored as readable service settings. |
service.volume_applied_mount_path | string | null | no | Last successfully confirmed Compute mount path; null means unknown, not proof of a changed path. |
service.volume_mount_path | string | null | no | Compute volume mount path; null or omitted means /data for legacy volumes. Changes apply on deployment. |
service.volume_gib | integer | null | no | compute/managed database — size (whole Gi) of the persistent data volume; null = no volume. Compute can attach/grow via PUT .../volume; managed databases are platform-managed in v1 |
service.pg_version | integer | null | no | postgres only — the Postgres MAJOR version the service runs (e.g. 16), known before the instance is ever connected to or woken. Pick client tooling (pg_dump/pg_restore/psql) of the same major. null: never recorded (a legacy row). The exact server_version of a RUNNING instance is on GET .../database/metrics |
service.template_deployment_id | string | null | no | the template deployment that created this service (survives branch clones); null for services created directly |
service.template_modified | boolean | no | true once the service was reconfigured away from its template-deployed spec |
service.template_code | string | null | no | the template this service was deployed from; null for services created directly, and for one whose deployment record has been pruned |
service.template_logo_url | string | null | no | the template's mark; null when the template is not in the registry (or carries no logo). Official templates: a commit-pinned jsDelivr URL (TEMPLATE_LOGO_HOST). Community templates: any https:// link the author set. Joined live rather than stored, because a republish moves it |
service.template_name | string | null | no | the template's name as the registry spells it, for a surface people read; null when the template is not in the registry, or its manifest omits a name. Read from the manifest the registry serves NOW, beside the logo, because a republish can rename it |
service.template_version | string | null | no | the template version this instance was deployed from; null for a service created directly. Read from the deployment record, so it does not move when the registry is republished |
service.template_latest_version | string | null | no | the version the registry serves for this template now; null when the registry does not serve it — absent, unpublished or taken down. The row still reports template_code and template_name in those cases, because attribution of a resource its owner deployed is not catalog visibility. For display beside template_version, never for deciding whether an update exists |
service.template_update_available | boolean | no | true when upgrading this service's template deployment to the version the registry serves now would actually change something about the services it runs: a setting the new manifest would write, an environment variable it would write differently, a service it would add, or a variable it now requires that this instance holds no value for. A republish that only moved presentation is not an update, because applying it would change nothing, and neither is a new version string on identical content, which is what every community republish produces. False whenever the answer cannot be determined: a template absent from the registry, unpublished, taken down, a manifest this version of the platform cannot parse, or a registry read that failed. False too while the deployment's own run has not succeeded, because what an unfinished deploy needs is a retry, not an upgrade |
service.source | object | no | compute only — where the running image comes from: an image somebody deployed (type=image) or a connected repository (type=github). Full resource: GET /projects/{id}/services/{serviceId}/source |
service.source.type | "image" | "github" | yes | |
service.source.owner | string | no | |
service.source.repo | string | no | |
service.source.branch | string | no | the repo branch this service tracks |
service.source.auto_deploy | boolean | no | a push to `branch` rebuilds and redeploys the service |
service.status | "creating" | "active" | "error" | "deleting" | "deleted" | no | |
service.created_at | string | no | |
source | object | no | Where a compute service’s running image comes from: an image somebody deployed (type=image, the absence of a connected source) or a connected repository (type=github). |
source.type | "image" | "github" | yes | |
source.image | string | null | no | type=image — the image the service runs; null when nothing has been deployed yet |
source.id | string | no | |
source.owner | string | no | |
source.repo | string | no | |
source.branch | string | no | the repo branch this service tracks; defaults to the repo’s default branch |
source.root_dir | string | null | no | subdirectory to build from (monorepo); null = repo root. Also decides which pushes deploy this service: the build context is this directory, so a push that changed nothing under it is skipped |
source.watch_paths | string[] | null | no | include list of gitignore patterns, matched against paths relative to the repository root (not to root_dir); a push deploys this service only when it changed a matching path. null/absent falls back to the root-directory rule: a service with root_dir deploys when a push changed something under it, one building from the repo root deploys on every push |
source.auto_deploy | boolean | no | a push to `branch` rebuilds and redeploys; always false for a public repo (no App, no webhooks) |
source.public | boolean | no | connected as a public repo, without a GitHub App installation |
source.installation_id | string | null | no | |
source.repo_id | string | null | no | GitHub repository id (bigint as string) |
source.build_command | string | null | no | custom build command for source builds without a Dockerfile; null uses automatic detection |
source.start_command | string | null | no | |
source.created_at | string | no | |
source.updated_at | string | no | |
source.last_build | object | null | no | the newest build of this service (null before the first) |
source.last_build.id | string | no | |
source.last_build.source_id | string | null | no | the service source that queued it; null once that source was disconnected |
source.last_build.project_id | string | no | |
source.last_build.service_id | string | null | no | |
source.last_build.branch_id | string | null | no | |
source.last_build.commit_sha | string | no | |
source.last_build.status | "queued" | "fetching" | "building" | "deploying" | "live" | "failed" | no | |
source.last_build.image_ref | string | null | no | |
source.last_build.error | string | null | no | |
source.last_build.created_at | string | no | |
source.last_build.updated_at | string | no | |
build | object | no | |
build.buildId | string | yes | |
build.queued | boolean | yes | false when this commit already has a build (dedup) |
build.serviceId | string | yes | |
nextActions | object[] | no | |
nextActions.op | string | yes | Neutral logical action id, e.g. "service.add" — NOT an operationId or a CLI/MCP tool name; each client maps it to its own surface. |
nextActions.reason | string | yes | Natural-language, human/LLM-facing "why do this now". |
nextActions.args | object | no | Suggested, flat named arguments; "<placeholder>"s where a value is unknown. |
nextActions.gated | boolean | no | True if the action passes a governance gate. |
202
Default Response
| Field | Type | Required | Description |
|---|---|---|---|
status | "approval_required" | no | |
approvalId | string | no | |
action | string | no | the gated action, or a comma-joined compound set — prefer `actions` |
actions | string[] | no | every capability this approval covers |
message | string | no | |
url | string | no | the console page where a project admin reviews this request |
nextActions | object[] | no | |
nextActions.op | string | yes | Neutral logical action id, e.g. "service.add" — NOT an operationId or a CLI/MCP tool name; each client maps it to its own surface. |
nextActions.reason | string | yes | Natural-language, human/LLM-facing "why do this now". |
nextActions.args | object | no | Suggested, flat named arguments; "<placeholder>"s where a value is unknown. |
nextActions.gated | boolean | no | True if the action passes a governance gate. |
400
Default Response
| Field | Type | Required | Description |
|---|---|---|---|
error | string | yes |
403
Default Response
| Field | Type | Required | Description |
|---|---|---|---|
error | string | yes |
409
the name is taken; or code private_name_taken: another service in the branch already derives the same private network name (a compute cache-redis beside a redis cache)
| Field | Type | Required | Description |
|---|---|---|---|
error | string | yes | |
code | string | no | stable machine-readable reason, e.g. private_lane_disabled, private_lane_unavailable_in_region, public_access_closed, credential_change_in_progress |
429
Default Response
| Field | Type | Required | Description |
|---|---|---|---|
error | string | yes |
501
the database provider cannot create the requested pgVersion yet — nothing was created
| Field | Type | Required | Description |
|---|---|---|---|
error | string | yes |
502
the provider could not provision the service — nothing was created; safe to retry
| Field | Type | Required | Description |
|---|---|---|---|
error | string | yes |