Deploy API
Create API tokens and trigger deployments programmatically from CI, a CMS, or any HTTP client.
The Deploy API lets you trigger a deployment of your project from outside the dashboard — from a CI pipeline, a CMS webhook, or any script that can make an HTTP request. You authenticate with a bearer token that you create on this page.
To open it, go to your project's Settings, and under Integrations click Deploy API.
Note: Managing deploy tokens requires an organization admin role. If you don't have it, you'll be redirected to the project settings page.
Create a token
- Click New token in the top-right.
- In the New deploy token dialog, fill in:
- Name — a label so you remember what the token is for (for example,
GitHub Actions production). It's also shown in deploy notifications. - Scope — choose one of:
- Live deployment — always deploys your project's deploy branch (typically
main) to the production URL. - Preview branch — deploys one specific branch to a preview URL. A Branch field appears for you to name it.
- Any branch in this project — the caller chooses the branch and whether it's live or preview, per request. Useful for A/B variants and CI scripts that build many branches under one token.
- Live deployment — always deploys your project's deploy branch (typically
- Expires in — Never, 7 days, 30 days, 90 days, or 1 year. After it expires, the token stops working and returns a "token expired" error.
- Name — a label so you remember what the token is for (for example,
- Click Create token.
Warning: The token is shown once, right after creation, in a highlighted box with a Copy button. Copy it now and store it somewhere safe — it won't be shown again. If you lose it, revoke it and create a new one.
Treat tokens like passwords: keep them out of source control, and give each integration its own token with the narrowest scope it needs.
Manage existing tokens
Each token appears in the Tokens list with its name, a masked prefix, what it deploys, when it was created, and when it was last used (or Never used). Expired tokens are flagged.
To revoke a token, click Revoke on its row and confirm. The token stops working immediately, so any integration using it will need a new one.
Trigger a deployment
The page shows the exact base URL for your project. It looks like this:
https://<api-host>/v1/projects/<your-project-slug>/deployments
There are two endpoints:
| Method | Path | Purpose |
|---|---|---|
POST |
/v1/projects/<slug>/deployments |
Trigger a new deployment |
GET |
/v1/projects/<slug>/deployments/<deployment-id> |
Poll deployment status |
Authenticate every request by sending your token in the Authorization header:
Authorization: Bearer pdt_...
Trigger with curl
curl -X POST https://<api-host>/v1/projects/<slug>/deployments \
-H "Authorization: Bearer pdt_..." \
-H "Content-Type: application/json" \
-H "Idempotency-Key: ci-build-${BUILD_ID}" \
-d '{"branch": "main", "triggeredBy": "ci-build-${BUILD_ID}"}'
Trigger with fetch (JavaScript)
const res = await fetch('https://<api-host>/v1/projects/<slug>/deployments', {
method: 'POST',
headers: {
'Authorization': 'Bearer pdt_...',
'Content-Type': 'application/json',
// Same value on every retry of this deploy — see Idempotency-Key below.
'Idempotency-Key': 'ci-build-42',
},
body: JSON.stringify({
branch: 'main', // omit for branch-scoped tokens
// isLive: false, // optional override; inferred from branch
triggeredBy: 'ci-build',
}),
})
const { deploymentId, statusUrl } = await res.json()
Trigger with PHP
$ch = curl_init('https://<api-host>/v1/projects/<slug>/deployments');
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Authorization: Bearer pdt_...',
'Content-Type: application/json',
// Same value on every retry of this deploy — see Idempotency-Key below.
'Idempotency-Key: ci-build-42',
]);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([
'branch' => 'main', // omit for branch-scoped tokens
'triggeredBy' => 'ci-build',
]));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$response = json_decode(curl_exec($ch), true);
curl_close($ch);
$deploymentId = $response['deploymentId'];
$statusUrl = $response['statusUrl'];
Trigger with Python
import requests
res = requests.post(
'https://<api-host>/v1/projects/<slug>/deployments',
headers={
'Authorization': 'Bearer pdt_...',
# Same value on every retry of this deploy — see Idempotency-Key below.
'Idempotency-Key': 'ci-build-42',
},
json={
'branch': 'main', # omit for branch-scoped tokens
# 'isLive': False, # optional override; inferred from branch
'triggeredBy': 'ci-build',
},
)
data = res.json()
deployment_id = data['deploymentId']
status_url = data['statusUrl']
Tip: The dashboard's API documentation section shows these same snippets pre-filled with your project's real URL — and, right after you create a token, with the token itself.
Request body
POST accepts an optional JSON body (Content-Type: application/json, max 8 KB):
| Field | Type | When | Description |
|---|---|---|---|
triggeredBy |
string | optional | Free-form label shown next to the token name in deploy notifications (max 100 chars). |
branch |
string | required for any branch tokens; ignored for branch-scoped tokens | The branch to deploy. |
isLive |
boolean | optional override | Defaults to true when branch matches the project's deploy branch (typically main), otherwise false. Pass it explicitly only for edge cases. |
For a branch-scoped token (Live deployment or Preview branch), you can omit the body entirely — the token already knows what to deploy.
Response
A successful POST returns 202 Accepted with the deployment id and the URL to poll:
{
"deploymentId": "01J...",
"statusUrl": "https://<api-host>/v1/projects/<slug>/deployments/01J..."
}
Poll deployment status
Send a GET to the statusUrl with the same bearer token:
curl https://<api-host>/v1/projects/<slug>/deployments/<deployment-id> \
-H "Authorization: Bearer pdt_..."
The response reports progress:
{
"deploymentId": "01J...",
"status": "running",
"createdAt": "2026-05-07T12:00:00.000Z",
"startedAt": "2026-05-07T12:00:01.000Z",
"completedAt": null,
"publishedUrl": null,
"errorMessage": null
}
The status field moves through:
pending → queued → running → completed
Or it ends in a terminal failure state:
failed— the build or deploy hit an error; seeerrorMessage.cancelled— a newer deployment for the same branch superseded this one.
When a deployment completes, publishedUrl points at the live result.
Error codes
Every error returns JSON shaped like {"error": {"code": "...", "message": "..."}}. Switch on
code in your scripts — the human-readable message may change.
| HTTP | Code | Meaning |
|---|---|---|
400 |
invalid_body |
Request body is malformed JSON or has wrong field types. |
400 |
body_too_large |
Request body exceeds 8 KB. |
400 |
branch_required |
Token is "any branch" but body has no "branch" field. |
400 |
invalid_branch |
Branch name fails validation (charset, length, ".." check). |
400 |
invalid_idempotency_key |
Idempotency-Key header has bad chars or is too long. |
401 |
missing_authorization |
No Authorization: Bearer header sent. |
401 |
invalid_token |
Token is wrong or has been revoked. |
401 |
token_rejected |
Token was rejected for a reason none of the codes above covers. Generate a new one. |
403 |
invalid_token_scope |
Token has no project scope. Generate a new one. |
403 |
token_not_for_project |
URL slug doesn't match the project this token is scoped to (or the project moved orgs). |
404 |
deployment_not_found |
No deployment with that ID for this project. |
409 |
builds_disabled |
Managed builds are disabled for this project — deploy it through your own CI instead. |
409 |
idempotency_conflict |
Same Idempotency-Key was previously used for a different branch, or for the other of live/preview. |
410 |
token_expired |
Token reached its expiry date. Generate a new one. |
410 |
token_disabled |
Token has been disabled. Generate a new one. |
429 |
rate_limited |
Token exceeded its 60 req/min rate limit. |
Good to know
-
Idempotency-Key. To make retries safe, send an
Idempotency-Keyheader. A secondPOSTwithin 24 hours using the same key (and same branch) returns the originaldeploymentIdinstead of creating a duplicate, and the replay carries anIdempotent-Replayed: trueheader.The key identifies the deploy you want, not the attempt. Every retry of the same deploy has to send the same value — a fresh UUID per attempt protects nothing, because each one asks for a different deploy. Derive it from whatever makes this deploy unique: a build id, a commit SHA, a content hash. A scheduled caller that has none of those can use the run it belongs to, e.g.
nightly-2026-08-24— the date of the run, not the current time. (1–200 chars, charsetA-Z a-z 0-9 ._:/+=-.)The key is tied to what it deployed. Reusing it for a different branch, or to flip a preview to live, answers
409 idempotency_conflictrather than quietly re-aiming a deployment that is still waiting.A replay returns the existing deployment instead of creating a second one. If that deployment is still waiting — an earlier attempt created it but never got it started — the replay puts the queue back in motion, so retrying is what recovers it. When the branch already has a deployment running, the replay changes nothing and answers
queued; poll the status URL rather than sending the request again. -
Rate limits. Each token is limited to 60 requests per minute; exceeding it returns
429 Too Many Requests. Bursts to the same branch are coalesced — only the latest deployment runs — while different branches deploy in parallel. -
Builds disabled. If the project has managed builds disabled, a
POSTreturns409with codebuilds_disabled— deploy the site through your own CI instead.
Related
- History and changes — see your deployments in the project's activity log.
- Hosting and domains — point a custom domain at your deployed site.