Skip to content

Deploy API ​

Internal Application API

Deploy hook management is an internal SPA route (session-authenticated). There is no public, key-gated deploy surface. All routes require a workspace owner or admin.

A deploy hook lets Studio rebuild and redeploy your site when content changes. Most static hosts expose a build hook (an HTTPS URL that rebuilds the site when it receives a POST): Netlify build hooks, Vercel deploy hooks, Cloudflare Pages deploy hooks, or any custom endpoint. Studio stores one hook per project.

All routes are project-scoped:

/api/workspaces/:workspaceId/projects/:projectId/deploy/...

When a Deploy Fires ​

ReasonWhenControlled by
content_publishedAfter content is merged into the project's content branch (a save that auto-merges, or a branch merge)triggers.on_publish
scheduleA scheduled publish_at / expire_at boundary passedtriggers.on_schedule
manualYou call Trigger a Deploy--

Automatic deploys (content_published) are debounced per project: the first merge after a quiet minute deploys at once, and further merges within the next 60 seconds are coalesced into one trailing deploy, sent when the minute is up. Scheduled and manual deploys fire immediately. Both triggers default to true.

Studio sends the hook a POST with a JSON body and a 15-second timeout:

json
{ "source": "contentrain-studio", "reason": "content_published", "at": "2026-01-15T12:00:00.000Z" }

Each attempt updates lastTriggeredAt and lastStatus (the hook's HTTP status, 0 when the request failed) and emits a deploy.triggered webhook event with provider, reason, ok and status in data.

TIP

The hook URL is a credential. Studio validates it (HTTPS only, no localhost or private-network hosts), stores it AES-GCM encrypted, and never returns it: responses carry only a hint.

Get Deploy Settings ​

GET /api/workspaces/:workspaceId/projects/:projectId/deploy

Response ​

json
{
  "target": {
    "provider": "netlify",
    "hookHint": "api.netlify.com/…a1b2",
    "triggers": { "on_publish": true, "on_schedule": true },
    "updatedAt": "2026-01-15T12:00:00.000Z",
    "lastTriggeredAt": "2026-01-15T12:30:00.000Z",
    "lastStatus": 200
  },
  "scheduled": [
    {
      "id": "uuid",
      "modelId": "blog-post",
      "entryId": "a1b2c3d4e5f6",
      "locale": "en",
      "kind": "publish",
      "fireAt": "2026-02-01T09:00:00.000Z"
    }
  ]
}

target is null when no hook is configured. scheduled lists the pending scheduled boundaries (kind is publish or expire). 404 project.not_found when the project is not in the workspace.


Set Deploy Hook ​

PATCH /api/workspaces/:workspaceId/projects/:projectId/deploy
FieldTypeRequiredDescription
provider'netlify' | 'vercel' | 'cloudflare-pages' | 'generic'NoHost label (defaults to the current provider, or generic)
hookUrlstringConditionalBuild hook URL. Required the first time; omit it later to keep the stored URL and change only provider or triggers.
triggers{ on_publish?: boolean, on_schedule?: boolean }NoWhich events fire the hook

Returns { "target": { ... } } in the same shape as above. Errors: 400 deploy.hook_url_invalid (not an HTTPS URL on a public host, or no URL on first setup), 400 deploy.provider_invalid.


Remove Deploy Hook ​

DELETE /api/workspaces/:workspaceId/projects/:projectId/deploy

Returns { "removed": true }.


Trigger a Deploy ​

Fire the hook now, bypassing the debounce.

POST /api/workspaces/:workspaceId/projects/:projectId/deploy/trigger

Returns the hook result:

json
{ "ok": true, "status": 200 }
StatusCodeCause
404deploy.not_configuredThe project has no deploy hook
502deploy.trigger_failedThe hook did not accept the request
403deploy.approval_requiredThe project uses the review workflow and the release still needs approval (the response data.approval holds the decision)
429At most 6 manual deploys per minute per project

Release Approval ​

On projects using the review workflow (plan feature workflow.review), a manual deploy is gated separately from content approval. Approving a branch answers whether the words are right; approving a release answers whether now is the moment to put them in front of the public. A release has its own plan, its own approval policy and its own approvals, and the approval policy decides how many and which approvers are required. When a deploy succeeds its approvals are cleared, so the next release needs a new decision. Projects not on the review workflow deploy without this step.

Approve the Next Release ​

POST /api/workspaces/:workspaceId/projects/:projectId/deploy/approve
FieldTypeRequiredDescription
notestringNoNote stored with your approval

Returns { "approval": { ... } }, the current release decision after your approval is recorded. 400 branches.approval_not_required when the project is not on the review workflow.

Withdraw Your Approval ​

DELETE /api/workspaces/:workspaceId/projects/:projectId/deploy/approve

Withdraws only your own approval. Returns { "approval": { ... } } with the updated decision.

Released under the AGPL-3.0 License.