Projects API
Create and configure projects, link a git repo, manage env vars and resources, read logs, deploy an uploaded folder, and transfer projects to and from Openship Cloud.
A project is the unit Openship builds and runs: a source (a git repo, a local path, or an uploaded
folder), its build configuration, environment variables, resource limits, domains, and deployment history.
In the dashboard this is everything under a project's pages; from the terminal it's the
openship projects command.
Base path & auth
All paths are relative to your instance, under /api — e.g. https://your-host/api/projects.
Send a personal access token as a bearer header (Authorization: Bearer <token>), created with
openship token create. The dashboard uses your session cookie instead. See the
API overview and auth model for details.
Both modes, with a few self-hosted-only routes
The Projects API is served on both self-hosted instances and Openship Cloud. On a self-hosted instance,
per-project routes transparently proxy to the SaaS for any project that lives in the cloud. Rows marked
† below are self-hosted only (local filesystem, edge route rules, folder-upload relay, and
cloud transfer) and return 404 on the SaaS.
Endpoints
| Method & path | Permission | What it does |
|---|---|---|
GET /api/projects | project:list | List projects in the org. |
POST /api/projects | project:write | Create a project from a git or local source (build config baked in). |
GET /api/projects/home | project:list | Home feed — local + cloud projects merged server-side. |
POST /api/projects/ensure | project:write | Create or update the project carrying build config (folder-upload step 3). |
GET /api/projects/local † | project:list | List local-filesystem projects. |
POST /api/projects/scan † | project:write | Scan a local folder to detect its build config. |
POST /api/projects/import † | project:write | Import a local folder as a project. |
GET /api/projects/:id | project:read | Get a project — config, source, routes, status. |
PATCH /api/projects/:id | project:write | Update a project's configuration. |
DELETE /api/projects/:id | project:admin | Delete a project. |
GET /api/projects/:id/info | project:read | Detailed info (runtime, build, source). |
GET /api/projects/:id/deletion-preview | project:read | Preview what deleting the project would remove. |
GET /api/projects/:id/environments | project:read | List environments (production / previews). |
POST /api/projects/:id/environments | project:write | Create an environment (e.g. a preview). |
POST /api/projects/:id/options | project:write | Set build/deploy options. |
POST /api/projects/:id/enable | project:write | Enable the project (allow deploys / bring online). |
POST /api/projects/:id/disable | project:write | Disable the project (pause deploys / take offline). |
POST /api/projects/:id/routing/retry | project:write | Retry syncing the free .opsh.io edge route (no rebuild). |
GET /api/projects/:id/env | project:read | List env vars (secret values masked). |
PATCH /api/projects/:id/env | project:write | Merge env var changes (upserts + deletes). |
GET /api/projects/:id/clone-token | project:read | Get the per-project git clone credential. |
PATCH /api/projects/:id/clone-token | project:admin | Set/replace the per-project clone credential. |
GET /api/projects/:id/git | project:read | Get the linked git repository info. |
GET /api/projects/:id/commit-status | project:read | Compare the deployed commit against remote HEAD. |
POST /api/projects/:id/git/link | project:write | Link a git repository to the project. |
GET /api/projects/:id/branches | project:read | List the linked repository's branches. |
POST /api/projects/:id/auto-deploy | project:write | Enable/disable auto-deploy on push. |
POST /api/projects/:id/webhook-domain | project:write | Set the domain used for git webhook delivery. |
POST /api/projects/:id/branch | project:write | Set the deploy branch. |
GET /api/projects/:id/resources | project:read | Get the CPU/RAM/disk resource config. |
PATCH /api/projects/:id/resources | project:write | Update CPU/RAM/disk, sleep mode, or port. |
POST /api/projects/:id/resources | project:write | Update resources (POST alias of the PATCH). |
POST /api/projects/:id/sleep-mode | project:write | Set sleep mode (auto_sleep / always_on). |
GET /api/projects/:id/deployments | project:deployment:list | List the project's deployments. |
POST /api/projects/:id/deployment-session | project:read | Open a read-only deployment/build session. |
POST /api/projects/:id/connect | project:write | Connect a custom domain as the primary domain. |
GET /api/projects/:id/logs | project:read | Fetch runtime logs (non-streaming). |
GET /api/projects/:id/logs/stream | project:read | Stream runtime logs. |
GET /api/projects/:id/server-logs/recent | project:read | Recent HTTP request logs. |
GET /api/projects/:id/server-logs/stream-token | project:read | Mint a token for the server-log stream. |
GET /api/projects/:id/server-logs/stream | project:read | Stream HTTP request logs. |
GET /api/projects/:id/route-rules † | project:read | List edge route rules (rate-limit / ban / allow-deny). |
POST /api/projects/:id/route-rules † | project:write | Create an edge route rule. |
PATCH /api/projects/:id/route-rules/:ruleId † | project:write | Update a route rule. |
DELETE /api/projects/:id/route-rules/:ruleId † | project:write | Delete a route rule. |
POST /api/projects/folder/session | project:write | Open a folder-upload session (step 1). |
POST /api/projects/folder/scan/:sessionId | project:write | Detect the uploaded source's build config (step 2). |
POST /api/projects/folder/upload/:sessionId † | project:write | Upload the gzipped tarball (self-hosted relay, 300 MB limit). |
POST /api/projects/:id/transfer/to-cloud † | project:admin | Promote a local project to Openship Cloud. |
POST /api/projects/:id/transfer/to-self-hosted † | project:admin | Bring a cloud project back to this instance. |
Create a project
POST /api/projectsCreate a project from a git repo or a local path, baking the build config into the project row. For an
uploaded folder, use the folder-upload flow instead. Only name is required —
everything else is auto-detected or defaulted.
Prop
Type
curl -X POST https://your-host/api/projects \
-H "Authorization: Bearer $OPENSHIP_TOKEN" \
-H "Content-Type: application/json" \
-d '{"name":"marketing-site","gitOwner":"acme","gitRepo":"site","gitBranch":"main","framework":"nextjs"}'# Same thing from the CLI:
openship projects createCreating a project does not deploy it
POST /api/projects only records the project and its config. Trigger the first build with the
Deployments API (POST /api/deployments) or openship deploy.
Update a project
PATCH /api/projects/:idThe update body is a partial of the create body — send only the fields you want to change. Every field above is accepted and optional here.
curl -X PATCH https://your-host/api/projects/proj_123 \
-H "Authorization: Bearer $OPENSHIP_TOKEN" \
-H "Content-Type: application/json" \
-d '{"buildCommand":"pnpm build","gitBranch":"release"}'openship projects update proj_123 --build-command "pnpm build"Link a git repo
POST /api/projects/:id/git/linkAttach (or re-point) a GitHub repository to an existing project. When branch is omitted, the repo's
default branch is used.
Prop
Type
curl -X POST https://your-host/api/projects/proj_123/git/link \
-H "Authorization: Bearer $OPENSHIP_TOKEN" \
-H "Content-Type: application/json" \
-d '{"owner":"acme","repo":"site","branch":"main"}'Related git routes: POST /api/projects/:id/branch (change the deploy branch),
POST /api/projects/:id/auto-deploy (toggle deploy-on-push), GET /api/projects/:id/branches, and
GET /api/projects/:id/commit-status (is the deployed commit behind remote HEAD?). See the
GitHub API for connecting the GitHub App itself.
Connect a custom domain
POST /api/projects/:id/connectA shortcut that adds a domain to the project and marks it primary in one call. For the full domain lifecycle (DNS preview, verification, SSL), use the Domains API.
Prop
Type
curl -X POST https://your-host/api/projects/proj_123/connect \
-H "Authorization: Bearer $OPENSHIP_TOKEN" \
-H "Content-Type: application/json" \
-d '{"domain":"app.example.com"}'Environment variables
GET /api/projects/:id/env
PATCH /api/projects/:id/envGET lists variables for the project with secret values masked. Writes go through a merge: only the
keys you name are touched — upserts are inserted/updated, deletes are removed, and every other
variable (including untouched masked secrets) is left intact.
Prop
Type
curl -X PATCH https://your-host/api/projects/proj_123/env \
-H "Authorization: Bearer $OPENSHIP_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"environment": "production",
"upserts": [{"key":"DATABASE_URL","value":"postgres://…","isSecret":true}],
"deletes": ["OLD_FLAG"]
}'openship env set DATABASE_URL="postgres://…" --project proj_123 --secretThere is no full-replace endpoint
The old destructive PUT /:id/env was removed — it could wipe or corrupt masked secrets. Always send a
diff via the merge PATCH.
Resources
GET /api/projects/:id/resources
PATCH /api/projects/:id/resources
POST /api/projects/:id/resourcesRead or update the CPU/RAM/disk allotment (separately for production and build), sleep behavior, and listen
port. POST is an alias of PATCH. All fields are optional; send only what you want to change.
Prop
Type
curl -X PATCH https://your-host/api/projects/proj_123/resources \
-H "Authorization: Bearer $OPENSHIP_TOKEN" \
-H "Content-Type: application/json" \
-d '{"production":{"cpuCores":1,"memoryMb":1024},"sleepMode":"always_on"}'Sleep mode also has its own shortcut route: POST /api/projects/:id/sleep-mode.
Create an environment
POST /api/projects/:id/environmentsProp
Type
Folder-upload deploy
Deploy a local folder from a browser or client that shares no filesystem with the API. The flow is four steps; the binary tarball upload happens out-of-band (raw bytes, not JSON).
1. Open a session
POST /api/projects/folder/session → returns an upload target
{ url, absoluteUrl, method, headers, requiresAuth }.
2. Upload the tarball
POST the gzipped tarball to upload.absoluteUrl with upload.headers and
Content-Type: application/gzip. On self-hosted this relays through
POST /api/projects/folder/upload/:sessionId (300 MB limit).
3. Scan
POST /api/projects/folder/scan/:sessionId detects stack, package manager, and build/start commands
— plus services for a docker-compose folder.
4. Ensure + deploy
Feed the scan into POST /api/projects/ensure (create/update the project), then deploy via
POST /api/deployments/build/access.
The upload target is deliberately opaque — it points at this API's relay on a self-hosted instance and
straight at a build workspace on Openship Cloud. Use absoluteUrl as-is, or resolve the API-relative
url against your own API base. When requiresAuth is true (the self-hosted relay), send the same
Authorization: Bearer <token> you used to open the session in addition to upload.headers — the
relay is permission-checked like every other route. When it's false, the target is external and
carries its own credential in headers; never add your Openship token to it.
The session body (POST /api/projects/folder/session) is a lightweight hint — all fields optional:
Prop
Type
POST /api/projects/ensure takes the full create body plus an optional projectId to
update an existing project in place instead of creating a new one. Set gitProvider: "upload" for a
folder-upload project.
Multi-service folders
When the uploaded folder has a docker-compose.yml, the scan returns a services array. Pass it through
to POST /api/projects/ensure verbatim and it becomes the project's service set — ensure owns those
rows, so send the whole set (services you leave out are removed, same as
POST /api/projects/:id/services/sync). If you skip it, the deploy step falls back to the services the
scan parsed for that upload session, so the four-step flow works either way; passing them explicitly is
what lets you review or edit the set before deploying.
Send uploadSessionId with echoed services
Compose environment values are masked on every read (••••••••). When you echo a scanned
services array back into ensure, include
uploadSessionId so the server can restore the real values from the scan it captured pre-mask. Without
it, masked values have no source to restore from and are dropped rather than persisted as dots (the
same rule every write path follows). Values you revealed and edited are sent in the clear and pass
through untouched.
Transfer to / from cloud
POST /api/projects/:id/transfer/to-cloud † self-hosted only
POST /api/projects/:id/transfer/to-self-hosted † self-hosted onlyPromote a local project to Openship Cloud, or pull a cloud project back to this instance. These are
meaningless on the SaaS (it is the cloud) and return 404 there. Both require project:admin.
curl -X POST https://your-host/api/projects/proj_123/transfer/to-cloud \
-H "Authorization: Bearer $OPENSHIP_TOKEN"Errors you might see
400 — validation failed
A field is missing or out of range (e.g. name empty, a cpuCores above 4, or git/link without
owner/repo). The response names the offending field.
403 — insufficient permission
Your token or role lacks the route's tag — deleting needs project:admin, writes need project:write.
See the permission model.
404 on a per-project route
The :id doesn't belong to your organization (or was deleted). A self-hosted-only route (marked †)
also returns 404 when called on Openship Cloud. List your projects with GET /api/projects to get valid
IDs.