# Register a signed deployment

POST

/v1/deployments

```sh
curl --request POST \
  --url https://example.com/v1/deployments \
  --header 'Authorization: Bearer <token>' \
  --header 'Content-Type: application/json' \
  --data '{ "imageRef": "example", "artifactVersion": "example", "indexHash": "example", "signerKeyId": "example", "signerPublicKey": "example", "signature": "example", "publishedAt": "2026-04-15T12:00:00Z", "projectId": "2489E9AD-2EE2-8E00-8EC9-32D5F69181C0" }'
```

Atomic transaction: check the signing key against the tenant’s trust list (the `signingKeyRegistry` binding), verify the Ed25519 signature over the canonical envelope, read the image’s `/app/index.json` and verify it hashes to the signed `indexHash` and names the signed `artifactVersion` (`ImageRegistryBinding`), validate every tool / guardrail / agent / flow it declares (the image’s index is what registers; the request carries none), upsert into the corresponding registries (in the body’s `projectId`, one project per client, else the tenant’s Default project), then record the deployment. All-or-nothing rollback on any failure. Idempotency: same `imageDigest` re-submitted returns 200 with the existing record (no new version bump). `Idempotency-Key` also applies at the HTTP layer.

## Authorizations

- **[bearerAuth](https://docs.kindgi.com/v0.1.8/reference/api.md#bearerauth)**

## Parameters

### Header Parameters

**Idempotency-Key**

string

\>= 1 characters

Caller-supplied idempotency key, scoped to the caller: the same key from someone else in the tenant is their own request (from 0.1.5). Retries with the same key return the original response byte-identical (per `docs/API-ROUTE-CONVENTIONS.md` §3.1). A retry sent while the first request still runs is answered `409 idempotency-key-in-flight` with `Retry-After`, on a runtime whose store holds keys (from 0.1.5); retry after it, and you get the first request’s answer. An answer that carries a secret (a new API key or public run token, a session token, a generated signing secret) isn’t kept: a retry gets `409 idempotency-key-replay-withheld`, with the first request’s status and when it succeeded (from 0.1.5).

## Request Bodyrequired

application/json

object

**imageRef**

required

Full digest-pinned OCI reference: `<host>/<repo>@sha256:<hex>`.

string

**artifactVersion**

required

Issuer-generated `YYYYMMDD.N` — covered by the signature.

string

/^\d{8}\\.\d+$/

**indexHash**

required

Sha256 of the image’s `/app/index.json`, byte for byte — covered by the signature. The server reads the file from the image, checks it hashes to this, and registers what it declares.

string

/^sha256:\[0-9a-f]{64}$/

**signerKeyId**

required

string

\>= 1 characters

**signerPublicKey**

required

PEM-encoded Ed25519 public key (`-----BEGIN PUBLIC KEY-----\n…`). Parsed to raw bytes for verification.

string

**signature**

required

Base64 of the Ed25519 signature over canonicalise({ imageDigest, artifactVersion, indexHash, tenantId, publishedAt }) with sorted keys, no whitespace, UTF-8.

string

**publishedAt**

required

Issuer-supplied ISO-8601 timestamp — covered by the signature.

string format: date-time

**projectId**

The project to deploy into, one per client: its agents, flows, tools and guardrails are registered there, and its secret sync writes there. It takes admin on that project (a key limited to another project is refused). Absent: a tenant-wide deploy, into the tenant’s Default project. An id that already lives in another project is `409` (`tool-project-mismatch` and the like): each client gets its own pack, whose ids carry its own pack id. Not covered by the signature, which attests the image.

string format: uuid

##### Examplegenerated

```json
{
  "imageRef": "example",
  "artifactVersion": "example",
  "indexHash": "example",
  "signerKeyId": "example",
  "signerPublicKey": "example",
  "signature": "example",
  "publishedAt": "2026-04-15T12:00:00Z",
  "projectId": "2489E9AD-2EE2-8E00-8EC9-32D5F69181C0"
}
```

## Responses

### 200

Digest replay — same imageDigest already recorded; returns existing record.

application/json

object

**deploymentId**

required

string

\>= 1 characters

**tenantId**

required

string format: uuid

**imageRef**

required

Full digest-pinned OCI reference: `<host>/<repo>@sha256:<hex>`.

string

**imageDigest**

required

Idempotency key. Extracted from `imageRef` after the `@`.

string

/^sha256:\[0-9a-f]{64}$/

**artifactVersion**

required

Issuer-supplied `YYYYMMDD.N` — matches what was signed.

string

/^\d{8}\\.\d+$/

**indexHash**

required

Sha256 of the canonicalised `/app/index.json` inside the image.

string

/^sha256:\[0-9a-f]{64}$/

**signerKeyId**

required

string

\>= 1 characters

**signerPublicKey**

required

Base64 of the raw Ed25519 public key bytes (32 bytes → 44 chars).

string

**signature**

required

Base64 of the raw Ed25519 signature (64 bytes → 88 chars).

string

**publishedAt**

required

Issuer-supplied timestamp inside the signed envelope.

string format: date-time

**activatedAt**

required

Server-side ledger timestamp — when the register call succeeded.

string format: date-time

**projectId**

The project the deploy named, which its primitives were registered into. Absent for a tenant-wide deploy (the tenant’s Default project).

string format: uuid

**endpoint**

Its pack service’s status, when the server routes each pack call to the deployment that shipped the tool (`KINDGI_PACK_ROUTING=per-deployment`). Absent otherwise. `pending`: no pack service yet, and calls to its tools fail (`pack-endpoint-pending`) rather than go to another deployment. `ready`: calls go to its own pack service. `cleared`: its pack service was taken down, and calls fail (`pack-endpoint-cleared`). Carries no URL and no token.

object

**status**

required

string

Allowed values: pending ready cleared

**since**

required

When the status last changed.

string format: date-time

**problem**

With `pending` only: the last reason the deployment has no pack service yet, gone once the status changes. A fixed code, never free text.

object

**code**

required

One of a fixed list; a later runtime may add codes, so treat an unknown one as a reason not yet named. The server refused the endpoint it was given: `endpoint-url-invalid` (not `https`, or with user info or a path), `endpoint-host-not-allowed` (the host isn’t on the operator’s allowlist), `endpoint-name-mismatch` (the host doesn’t name this deployment’s service), `endpoint-unreachable` (the pack service didn’t answer), `endpoint-info-mismatch` (it answered as another pack or version), `image-not-allowed` (the image is outside the tenant’s repository). The pack service couldn’t be created: `tenant-not-provisioned` (the tenant has nowhere to run pack services yet), `capacity-unavailable` (no room for another pack service just now), `service-start-failed` (the image was deployed but didn’t start).

string

**since**

required

When it was recorded.

string format: date-time

**primitives**

required

object

**tools**

required

integer

**guardrails**

required

integer

**agents**

required

integer

**flows**

required

integer

**contents**

required

object

**tools**

required

Array\<object>

object

**id**

required

string

**version**

Absent for guardrails, which have no version.

string

**guardrails**

required

Array\<object>

object

**id**

required

string

**version**

Absent for guardrails, which have no version.

string

**agents**

required

Array\<object>

object

**id**

required

string

**version**

required

The version the agent is registered as.

string

**authoredVersion**

The version the agent’s or flow’s definition names, present when it differs from `version`: that version was registered already with other pins or content, and versions never change, so the deploy registered the next free version in its line (or an earlier deploy did).

string

**reason**

Why `version` differs from `authoredVersion`: `pins-changed` (a tool or agent it uses has a new version), `unpinned` (`authoredVersion` was published before pins existed), `version-taken` (`authoredVersion` is registered with other content).

string

Allowed values: pins-changed unpinned version-taken

**newVersion**

`true`: this deploy registered `version`; `false`: an earlier deploy did.

boolean

**pinChanges**

For `pins-changed`: the pins that differ from `authoredVersion`’s.

Array\<object>

One pin that differs between two versions of an agent or a flow.

object

**kind**

required

string

Allowed values: tool prompt setting agent

**id**

required

string

**from**

The earlier version’s pin; absent when it had none.

string

**to**

The later version’s pin; absent when it has none.

string

**flows**

required

Array\<object>

object

**id**

required

string

**version**

required

The version the agent is registered as.

string

**authoredVersion**

The version the agent’s or flow’s definition names, present when it differs from `version`: that version was registered already with other pins or content, and versions never change, so the deploy registered the next free version in its line (or an earlier deploy did).

string

**reason**

Why `version` differs from `authoredVersion`: `pins-changed` (a tool or agent it uses has a new version), `unpinned` (`authoredVersion` was published before pins existed), `version-taken` (`authoredVersion` is registered with other content).

string

Allowed values: pins-changed unpinned version-taken

**newVersion**

`true`: this deploy registered `version`; `false`: an earlier deploy did.

boolean

**pinChanges**

For `pins-changed`: the pins that differ from `authoredVersion`’s.

Array\<object>

One pin that differs between two versions of an agent or a flow.

object

**kind**

required

string

Allowed values: tool prompt setting agent

**id**

required

string

**from**

The earlier version’s pin; absent when it had none.

string

**to**

The later version’s pin; absent when it has none.

string

##### Example

```json
{
  "endpoint": {
    "status": "pending"
  },
  "contents": {
    "agents": [
      {
        "reason": "pins-changed",
        "pinChanges": [
          {
            "kind": "tool"
          }
        ]
      }
    ],
    "flows": [
      {
        "reason": "pins-changed",
        "pinChanges": [
          {
            "kind": "tool"
          }
        ]
      }
    ]
  }
}
```

### 201

Deployment registered.

application/json

object

**deploymentId**

required

string

\>= 1 characters

**tenantId**

required

string format: uuid

**imageRef**

required

Full digest-pinned OCI reference: `<host>/<repo>@sha256:<hex>`.

string

**imageDigest**

required

Idempotency key. Extracted from `imageRef` after the `@`.

string

/^sha256:\[0-9a-f]{64}$/

**artifactVersion**

required

Issuer-supplied `YYYYMMDD.N` — matches what was signed.

string

/^\d{8}\\.\d+$/

**indexHash**

required

Sha256 of the canonicalised `/app/index.json` inside the image.

string

/^sha256:\[0-9a-f]{64}$/

**signerKeyId**

required

string

\>= 1 characters

**signerPublicKey**

required

Base64 of the raw Ed25519 public key bytes (32 bytes → 44 chars).

string

**signature**

required

Base64 of the raw Ed25519 signature (64 bytes → 88 chars).

string

**publishedAt**

required

Issuer-supplied timestamp inside the signed envelope.

string format: date-time

**activatedAt**

required

Server-side ledger timestamp — when the register call succeeded.

string format: date-time

**projectId**

The project the deploy named, which its primitives were registered into. Absent for a tenant-wide deploy (the tenant’s Default project).

string format: uuid

**endpoint**

Its pack service’s status, when the server routes each pack call to the deployment that shipped the tool (`KINDGI_PACK_ROUTING=per-deployment`). Absent otherwise. `pending`: no pack service yet, and calls to its tools fail (`pack-endpoint-pending`) rather than go to another deployment. `ready`: calls go to its own pack service. `cleared`: its pack service was taken down, and calls fail (`pack-endpoint-cleared`). Carries no URL and no token.

object

**status**

required

string

Allowed values: pending ready cleared

**since**

required

When the status last changed.

string format: date-time

**problem**

With `pending` only: the last reason the deployment has no pack service yet, gone once the status changes. A fixed code, never free text.

object

**code**

required

One of a fixed list; a later runtime may add codes, so treat an unknown one as a reason not yet named. The server refused the endpoint it was given: `endpoint-url-invalid` (not `https`, or with user info or a path), `endpoint-host-not-allowed` (the host isn’t on the operator’s allowlist), `endpoint-name-mismatch` (the host doesn’t name this deployment’s service), `endpoint-unreachable` (the pack service didn’t answer), `endpoint-info-mismatch` (it answered as another pack or version), `image-not-allowed` (the image is outside the tenant’s repository). The pack service couldn’t be created: `tenant-not-provisioned` (the tenant has nowhere to run pack services yet), `capacity-unavailable` (no room for another pack service just now), `service-start-failed` (the image was deployed but didn’t start).

string

**since**

required

When it was recorded.

string format: date-time

**primitives**

required

object

**tools**

required

integer

**guardrails**

required

integer

**agents**

required

integer

**flows**

required

integer

**contents**

required

object

**tools**

required

Array\<object>

object

**id**

required

string

**version**

Absent for guardrails, which have no version.

string

**guardrails**

required

Array\<object>

object

**id**

required

string

**version**

Absent for guardrails, which have no version.

string

**agents**

required

Array\<object>

object

**id**

required

string

**version**

required

The version the agent is registered as.

string

**authoredVersion**

The version the agent’s or flow’s definition names, present when it differs from `version`: that version was registered already with other pins or content, and versions never change, so the deploy registered the next free version in its line (or an earlier deploy did).

string

**reason**

Why `version` differs from `authoredVersion`: `pins-changed` (a tool or agent it uses has a new version), `unpinned` (`authoredVersion` was published before pins existed), `version-taken` (`authoredVersion` is registered with other content).

string

Allowed values: pins-changed unpinned version-taken

**newVersion**

`true`: this deploy registered `version`; `false`: an earlier deploy did.

boolean

**pinChanges**

For `pins-changed`: the pins that differ from `authoredVersion`’s.

Array\<object>

One pin that differs between two versions of an agent or a flow.

object

**kind**

required

string

Allowed values: tool prompt setting agent

**id**

required

string

**from**

The earlier version’s pin; absent when it had none.

string

**to**

The later version’s pin; absent when it has none.

string

**flows**

required

Array\<object>

object

**id**

required

string

**version**

required

The version the agent is registered as.

string

**authoredVersion**

The version the agent’s or flow’s definition names, present when it differs from `version`: that version was registered already with other pins or content, and versions never change, so the deploy registered the next free version in its line (or an earlier deploy did).

string

**reason**

Why `version` differs from `authoredVersion`: `pins-changed` (a tool or agent it uses has a new version), `unpinned` (`authoredVersion` was published before pins existed), `version-taken` (`authoredVersion` is registered with other content).

string

Allowed values: pins-changed unpinned version-taken

**newVersion**

`true`: this deploy registered `version`; `false`: an earlier deploy did.

boolean

**pinChanges**

For `pins-changed`: the pins that differ from `authoredVersion`’s.

Array\<object>

One pin that differs between two versions of an agent or a flow.

object

**kind**

required

string

Allowed values: tool prompt setting agent

**id**

required

string

**from**

The earlier version’s pin; absent when it had none.

string

**to**

The later version’s pin; absent when it has none.

string

##### Example

```json
{
  "endpoint": {
    "status": "pending"
  },
  "contents": {
    "agents": [
      {
        "reason": "pins-changed",
        "pinChanges": [
          {
            "kind": "tool"
          }
        ]
      }
    ],
    "flows": [
      {
        "reason": "pins-changed",
        "pinChanges": [
          {
            "kind": "tool"
          }
        ]
      }
    ]
  }
}
```

### 400

Signature invalid, image unverifiable, or deployment-validation-failed with per-primitive `details[]`. A tool that declares or sends a model provider’s key is one such issue (`path` `/secrets/<name>`): a tool never gets a model provider’s key. A tool whose `metadata` has a key under `kindgi.` (the runtime’s own) is a `validation-failed` detail at `/metadata/<key>`. Or `reference-out-of-scope`: an agent or flow names a tool, agent, flow, guardrail or data block that neither the deployment brings nor its project reaches (`details.issues`, at `/agents/<i>/…` or `/flows/<i>/…`); nothing is registered.

application/json

object

**error**

required

object

**code**

required

Stable machine-readable discriminant. Values match domain error codes (see `docs/API-ROUTE-CONVENTIONS.md` §4.3).

string

**message**

required

string

**details**

Optional, kind-specific.

object

**_key_**

additional properties

any

**requestId**

required

Server-assigned request id; also echoed via `X-Request-Id` header.

string

##### Examplegenerated

```json
{
  "error": {
    "code": "example",
    "message": "example",
    "details": {},
    "requestId": "example"
  }
}
```

### 401

Missing / malformed / expired / revoked bearer token.

application/json

object

**error**

required

object

**code**

required

Stable machine-readable discriminant. Values match domain error codes (see `docs/API-ROUTE-CONVENTIONS.md` §4.3).

string

**message**

required

string

**details**

Optional, kind-specific.

object

**_key_**

additional properties

any

**requestId**

required

Server-assigned request id; also echoed via `X-Request-Id` header.

string

##### Examplegenerated

```json
{
  "error": {
    "code": "example",
    "message": "example",
    "details": {},
    "requestId": "example"
  }
}
```

### 403

Signer key not on the tenant’s trust list. Or, with `projectId`, no admin on that project (`forbidden`), or a key limited to another project (`key-project-mismatch`); without it, no admin on the tenant.

application/json

object

**error**

required

object

**code**

required

Stable machine-readable discriminant. Values match domain error codes (see `docs/API-ROUTE-CONVENTIONS.md` §4.3).

string

**message**

required

string

**details**

Optional, kind-specific.

object

**_key_**

additional properties

any

**requestId**

required

Server-assigned request id; also echoed via `X-Request-Id` header.

string

##### Examplegenerated

```json
{
  "error": {
    "code": "example",
    "message": "example",
    "details": {},
    "requestId": "example"
  }
}
```

### 404

The body’s `projectId` names no project of this tenant (`project-not-found`).

application/json

object

**error**

required

object

**code**

required

Stable machine-readable discriminant. Values match domain error codes (see `docs/API-ROUTE-CONVENTIONS.md` §4.3).

string

**message**

required

string

**details**

Optional, kind-specific.

object

**_key_**

additional properties

any

**requestId**

required

Server-assigned request id; also echoed via `X-Request-Id` header.

string

##### Examplegenerated

```json
{
  "error": {
    "code": "example",
    "message": "example",
    "details": {},
    "requestId": "example"
  }
}
```

### 409

Idempotency-Key was reused with a different body, or resource-state conflict. Or, on a runtime serving many tenants (`KINDGI_TENANCY=multi`), where each tenant deploys only its own images: `image-not-allowed`, the image is outside the tenant’s repository (`details.imagePrefix`, its own), or `image-prefix-unset`, the tenant has no repository set yet; both are checked before any registry read, and never on a runtime serving one tenant. Or `tool-project-mismatch`, `agent-project-mismatch` or `flow-project-mismatch`: one of the image’s tools, agents or flows belongs to another project (`details.primitive`, `details.id`; the message doesn’t name the project); nothing was deployed, even when it was unchanged. Or `project-suspended`: the project the deploy goes into (the named one, or the Default for a tenant-wide deploy) is suspended, and nothing deploys into it until it’s restored. Or `registry-read-only`: this registry takes no writes (under `kindgi dev`, the pack’s files are the source); the message says what to do instead.

application/json

object

**error**

required

object

**code**

required

Stable machine-readable discriminant. Values match domain error codes (see `docs/API-ROUTE-CONVENTIONS.md` §4.3).

string

**message**

required

string

**details**

Optional, kind-specific.

object

**_key_**

additional properties

any

**requestId**

required

Server-assigned request id; also echoed via `X-Request-Id` header.

string

##### Examplegenerated

```json
{
  "error": {
    "code": "example",
    "message": "example",
    "details": {},
    "requestId": "example"
  }
}
```

### 413

The request body is over the API’s limit (`request-too-large`; `details.maxBytes` says how much).

application/json

object

**error**

required

object

**code**

required

Stable machine-readable discriminant. Values match domain error codes (see `docs/API-ROUTE-CONVENTIONS.md` §4.3).

string

**message**

required

string

**details**

Optional, kind-specific.

object

**_key_**

additional properties

any

**requestId**

required

Server-assigned request id; also echoed via `X-Request-Id` header.

string

##### Examplegenerated

```json
{
  "error": {
    "code": "example",
    "message": "example",
    "details": {},
    "requestId": "example"
  }
}
```

### 429

`rate-limit-exceeded` with `details.limit: "schema-compile-time"`, on a server with a compile budget (Kindgi Cloud): this tenant’s schemas spent `details.budgetMs` compiling in the last 60 seconds. Nothing was stored; `Retry-After` says when to send it again.

application/json

object

**error**

required

object

**code**

required

Stable machine-readable discriminant. Values match domain error codes (see `docs/API-ROUTE-CONVENTIONS.md` §4.3).

string

**message**

required

string

**details**

Optional, kind-specific.

object

**_key_**

additional properties

any

**requestId**

required

Server-assigned request id; also echoed via `X-Request-Id` header.

string

##### Examplegenerated

```json
{
  "error": {
    "code": "example",
    "message": "example",
    "details": {},
    "requestId": "example"
  }
}
```

### 500

Server error (unmapped domain code or framework crash).

application/json

object

**error**

required

object

**code**

required

Stable machine-readable discriminant. Values match domain error codes (see `docs/API-ROUTE-CONVENTIONS.md` §4.3).

string

**message**

required

string

**details**

Optional, kind-specific.

object

**_key_**

additional properties

any

**requestId**

required

Server-assigned request id; also echoed via `X-Request-Id` header.

string

##### Examplegenerated

```json
{
  "error": {
    "code": "example",
    "message": "example",
    "details": {},
    "requestId": "example"
  }
}
```

## See also

- [`GET /v1/deployments`: List signed deployments](https://docs.kindgi.com/v0.1.8/reference/api/operations/deploymentslist.md)
- [Error code `agent-project-mismatch`](https://docs.kindgi.com/v0.1.8/reference/errors.md#agent-project-mismatch)
- [Error code `flow-project-mismatch`](https://docs.kindgi.com/v0.1.8/reference/errors.md#flow-project-mismatch)
- [Error code `image-not-allowed`](https://docs.kindgi.com/v0.1.8/reference/errors.md#image-not-allowed)
- [Error code `image-prefix-unset`](https://docs.kindgi.com/v0.1.8/reference/errors.md#image-prefix-unset)
- [Error code `key-project-mismatch`](https://docs.kindgi.com/v0.1.8/reference/errors.md#key-project-mismatch)
- [Error code `project-not-found`](https://docs.kindgi.com/v0.1.8/reference/errors.md#project-not-found)
- [Error code `project-suspended`](https://docs.kindgi.com/v0.1.8/reference/errors.md#project-suspended)
- [Error code `rate-limit-exceeded`](https://docs.kindgi.com/v0.1.8/reference/errors.md#rate-limit-exceeded)
- [Error code `reference-out-of-scope`](https://docs.kindgi.com/v0.1.8/reference/errors.md#reference-out-of-scope)
- [Error code `registry-read-only`](https://docs.kindgi.com/v0.1.8/reference/errors.md#registry-read-only)
- [Error code `request-too-large`](https://docs.kindgi.com/v0.1.8/reference/errors.md#request-too-large)
- [Error code `tool-project-mismatch`](https://docs.kindgi.com/v0.1.8/reference/errors.md#tool-project-mismatch)
- [Error code `validation-failed`](https://docs.kindgi.com/v0.1.8/reference/errors.md#validation-failed)
- [Every error code](https://docs.kindgi.com/v0.1.8/reference/errors.md)
