# Complete deployments — POST /v1/projects/{slug}/deployments/{id}/complete

URL: https://docs.hanzo.ai/docs/openapi/project/post-projects-by-slug-deployments-by-id-complete

> The CI completion hook that flips a queued git deployment to live (or error) once CI has synced the built site to S3.

`POST /v1/projects/{slug}/deployments/{id}/complete`

> [Project →](https://docs.hanzo.ai/docs/openapi/project) · [All API references →](https://docs.hanzo.ai/docs/openapi) · [Get started →](https://docs.hanzo.ai/docs/guides)

|               |                                                                     |
| ------------- | ------------------------------------------------------------------- |
| **Address**   | `https://api.hanzo.ai/v1/projects/{slug}/deployments/{id}/complete` |
| **Method**    | `POST`                                                              |
| **Operation** | `post_projects_by_slug_deployments_by_id_complete`                  |
| **Auth**      | `Authorization: Bearer $HANZO_API_KEY`                              |

The CI completion hook that flips a queued git
deployment to live (or error) once CI has synced the built site to S3.

`status` must be `live` or `error`. On a LIVE completion the public host is
claimed FIRST, so the deployment reports the URL it actually OWNS — a
CI-supplied `liveUrl` is a hint that can refine that URL but can never assert
a subdomain another tenant holds. `keys` is the manifest CI just uploaded,
relative to the deployment prefix: cloud reconciles the prefix against it so a
page deleted from the build actually stops serving. Omit `keys` and nothing is
deleted — the prefix only grows. Reconciliation runs only on a live completion
(pruning against a failed build's manifest would delete the site the last good
build is still serving) and is best-effort, so a stale leftover never turns a
successful deploy into a 500. No completion is charged: the site is billed for
the storage it holds, and a live completion re-measures its org.

Scope: a validated principal is required (401 without one). CI authenticates
with an org-scoped token through the gateway, so the deployment is resolved
within that principal's org and another tenant's slug or deployment id is a
404\.

## Request

13 fields, body `application/json` (required).

| Field      | In   | Type            | Required | Description                                                                               |
| ---------- | ---- | --------------- | -------- | ----------------------------------------------------------------------------------------- |
| `slug`     | path | string          | yes      | Slug is the project the deployment belongs to, from the path.                             |
| `id`       | path | string          | yes      | ID is the queued deployment to complete, from the path.                                   |
| `bytes`    | body | integer (int64) | —        | Bytes is their total size in bytes.                                                       |
| `commit`   | body | string          | —        | Commit is the revision that was built, recorded on the deployment.                        |
| `copy`     | body | string\[]       | —        | Copy lists the keys to take from CopyFrom's prefix, relative to it.                       |
| `copyFrom` | body | string          | —        | CopyFrom names another site of the same org whose prefix already holds the bytes of Copy. |
| `files`    | body | integer (int64) | —        | Files is how many objects CI published.                                                   |
| `id`       | body | string          | —        | ID is the queued deployment to complete, from the path.                                   |
| `keys`     | body | string\[]       | —        | Keys is the manifest CI just uploaded, RELATIVE to the deployment prefix.                 |
| `liveUrl`  | body | string          | —        | LiveURL is a HINT at the address the site should serve at.                                |
| `message`  | body | string          | —        | Message is what happened, in words — on an error completion, why it failed.               |
| `slug`     | body | string          | —        | Slug is the project the deployment belongs to, from the path.                             |
| `status`   | body | string          | —        | Status is how the build ended: `live` if it succeeded, `error` if it did not.             |

## Response

| Status    | Body                       | Meaning |
| --------- | -------------------------- | ------- |
| `200`     | project.projectsDeployment | ok      |
| `default` | problem-details            | refused |

`200` body — 22 fields.

| Field              | In   | Type                        | Always | Description                                                                                                                                                                             |
| ------------------ | ---- | --------------------------- | ------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `bucket`           | body | string                      | —      | Bucket is the object-store bucket its files were written to.                                                                                                                            |
| `bytes`            | body | integer (int64)             | —      | Bytes is their total size in bytes.                                                                                                                                                     |
| `commit`           | body | string                      | —      | Commit is the revision that was built, for a deployment that came from a repository.                                                                                                    |
| `createdAt`        | body | integer (int64)             | —      | CreatedAt is when the deployment was queued, as Unix seconds.                                                                                                                           |
| `files`            | body | integer (int64)             | —      | Files is how many objects the deployment published.                                                                                                                                     |
| `id`               | body | string                      | —      | ID identifies this one deployment attempt, and is what CI quotes back to complete it.                                                                                                   |
| `liveUrl`          | body | string                      | —      | LiveURL is where this deployment serves, once it is live.                                                                                                                               |
| `message`          | body | string                      | —      | Message is what happened, in words — the build's own note, or on a failure why it failed.                                                                                               |
| `prefix`           | body | string                      | —      | Prefix is the key prefix within that bucket holding EXACTLY this deployment's objects — the unit an upload grant is scoped to, so a grant for one deployment cannot write over another. |
| `projectId`        | body | string                      | —      | ProjectID is the project this deployment belongs to.                                                                                                                                    |
| `source`           | body | string                      | —      | Source is what caused the deployment — a git push, an uploaded artifact, a generated site.                                                                                              |
| `status`           | body | string                      | —      | Status is where the attempt got to — queued, live, or failed.                                                                                                                           |
| `updatedAt`        | body | integer (int64)             | —      | UpdatedAt is when it last changed state, as Unix seconds — so the gap between the two is how long the build took.                                                                       |
| `upload`           | body | project.projectsUploadGrant | —      |                                                                                                                                                                                         |
| `upload.copy`      | body | boolean                     | —      | Copy is true when the completion of this deployment accepts copyFrom and copy.                                                                                                          |
| `upload.expiresAt` | body | integer (int64)             | —      | ExpiresAt is when the grant stops being accepted, as Unix seconds.                                                                                                                      |
| `upload.fields`    | body | object                      | —      | Fields are form values every POST must carry VERBATIM, alongside `key` and `file`.                                                                                                      |
| `upload.fields.*`  | body | string                      | —      |                                                                                                                                                                                         |
| `upload.maxBytes`  | body | integer (int64)             | —      | MaxBytes bounds ONE object, not the upload as a whole.                                                                                                                                  |
| `upload.prefix`    | body | string                      | —      | Prefix is the only place this grant can write: the deployment's own key prefix.                                                                                                         |
| `upload.url`       | body | string                      | —      | URL is the address to POST each object to.                                                                                                                                              |
| `version`          | body | integer (int64)             | —      | Version counts deployments of this project from 1, so the history reads as an ordered sequence rather than by timestamp.                                                                |

Failure carries the platform error shape — see [Errors](https://docs.hanzo.ai/docs/errors).

## Examples

**CLI**

```bash
hanzo projects deployments complete <slug> <id>
```

**SDK**

**.ts**

```ts
import { Configuration, ProjectApi } from 'hanzoai';

const api = new ProjectApi(new Configuration({ accessToken: process.env.HANZO_API_KEY }));
const { data } = await api.postProjectsBySlugDeploymentsByIdComplete({ slug: 'slug', id: 'id', bytes: 0, commit: "<commit>" });
```

**.py**

```python
from hanzoai.cloud import ApiClient, Configuration
from hanzoai.cloud.api import ProjectApi

client = ApiClient(Configuration(access_token=os.environ["HANZO_API_KEY"]))
result = ProjectApi(client).post_projects_by_slug_deployments_by_id_complete(slug='slug', id='id', bytes=0, commit="<commit>")
```

**.go**

```go
cfg := hanzoai.NewConfiguration()
cfg.AddDefaultHeader("Authorization", "Bearer "+os.Getenv("HANZO_API_KEY"))
client := hanzoai.NewAPIClient(cfg)

resp, _, err := client.ProjectAPI.PostProjectsBySlugDeploymentsByIdComplete(context.Background()).Execute()
if err != nil {
	return err
}
```

**.rs**

```rust
use hanzo_client::apis::{configuration::Configuration, project_api};

let mut cfg = Configuration::new();
cfg.bearer_access_token = std::env::var("HANZO_API_KEY").ok();

let result = project_api::post_projects_by_slug_deployments_by_id_complete(&cfg, Default::default()).await?;
```

**.java**

```java
import ai.hanzo.cloud.ApiClient;
import ai.hanzo.cloud.api.ProjectApi;

ApiClient client = new ApiClient();
client.setBearerToken(System.getenv("HANZO_API_KEY"));

var result = new ProjectApi(client).postProjectsBySlugDeploymentsByIdComplete();
```

The method above is the one at the current release of the document. `hanzoai@8.5.914` (npm) and `hanzoai@8.5.914` (PyPI) were generated from an earlier release, where this operation carried a different id, so it spells the method differently — regenerating the clients is what makes the two agree. [SDKs →](https://docs.hanzo.ai/docs/sdks)

**HTTP**

```bash
curl -X POST https://api.hanzo.ai/v1/projects/<slug>/deployments/<id>/complete \
  -H "Authorization: Bearer $HANZO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
       "bytes": 0,
       "commit": "<commit>"
     }'
```

**MCP**

Tool `project`, op `post_projects_by_slug_deployments_by_id_complete` — POST the JSON-RPC envelope to `https://api.hanzo.ai/v1/mcp`.

```bash
curl -X POST https://api.hanzo.ai/v1/mcp \
  -H "Authorization: Bearer $HANZO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
       "jsonrpc": "2.0",
       "id": 1,
       "method": "tools/call",
       "params": {
         "name": "project",
         "arguments": {
           "op": "post_projects_by_slug_deployments_by_id_complete",
           "input": {
             "slug": "<slug>",
             "id": "<id>",
             "bytes": 0,
             "commit": "<commit>"
           }
         }
       }
     }'
```

---

[Project API](https://docs.hanzo.ai/docs/openapi/project) · [All Hanzo APIs](https://docs.hanzo.ai/docs/openapi) · [Interactive reference](https://docs.hanzo.ai/docs/openapi/reference)
