---
title: "Use the API"
description: "Create secrets, write and read versions, and destroy versions with the Secrets API."
---

> Documentation Index
> Fetch the complete documentation index at: https://docs.ezghcloud.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Use the API

All paths are under `https://secrets.ezghcloud.com/v1/organizations/{organizationId}/projects/{projectId}`.
`{secretId}` is the secret's ID (a UUID), and `{version}` is a number or `latest`. The examples
use `$BASE` for that prefix and an [API key](/api/authentication).
Lists are paged as described in [Requests](/api/requests).

| Task | Operation |
| --- | --- |
| Create a secret | [CreateSecret](/secrets-api/CreateSecret/) |
| List secrets, or find one by `name` | [ListSecrets](/secrets-api/ListSecrets/) |
| Read a secret's metadata | [GetSecret](/secrets-api/GetSecret/) |
| Change description and tags | [UpdateSecret](/secrets-api/UpdateSecret/) |
| Delete a secret | [DeleteSecret](/secrets-api/DeleteSecret/) |
| Write a version | [CreateSecretVersion](/secrets-api/CreateSecretVersion/) |
| List versions | [ListSecretVersions](/secrets-api/ListSecretVersions/) |
| Read a version's metadata | [GetSecretVersion](/secrets-api/GetSecretVersion/) |
| Read a version's values | [AccessSecretVersion](/secrets-api/AccessSecretVersion/) |
| Read one field | [AccessSecretField](/secrets-api/AccessSecretField/) |
| Destroy a version | [DestroySecretVersion](/secrets-api/DestroySecretVersion/) |
| Manage grants | [CreateSecretGrant](/secrets-api/CreateSecretGrant/), [ListSecretGrants](/secrets-api/ListSecretGrants/), [GetSecretGrant](/secrets-api/GetSecretGrant/), [DeleteSecretGrant](/secrets-api/DeleteSecretGrant/) |

## Create a secret

```sh
curl -X POST "$BASE/secrets" \
  -H "Authorization: Bearer $EZGH_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "prod/db",
    "description": "Production database",
    "tags": { "team": "data" },
    "fields": { "DB_USER": "app", "DB_PASSWORD": "s3cret", "CA_CERT": { "base64": "LS0tLS1CRUdJTi..." } }
  }'
```

`201` returns the secret: `id`, `slug`, `name`, `resourceName`, `latestVersion` (a number, or
`null` for a secret created without `fields`), `versionCount`, `fieldCount` (the fields in
enabled versions, which is what you're billed for), `grantCount` and the timestamps. A secret is
never returned with a value. With `fields`, version 1 is created with the secret, which needs
`secrets.versions.create` as well as `secrets.secrets.create`. A name that's
taken is `409 secret_name_taken`. The create accepts an `Idempotency-Key`.

## Write a version

A version is either the complete set of fields (this one becomes version 2):

```sh
curl -X POST "$BASE/secrets/$SECRET_ID/versions" \
  -H "Authorization: Bearer $EZGH_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "fields": { "DB_USER": "app", "DB_PASSWORD": "s3cret", "OLD_KEY": "x" } }'
```

or changes to an existing version, which keeps every field you don't mention. This one builds
on `latest` and becomes version 3:

```sh
curl -X POST "$BASE/secrets/$SECRET_ID/versions" \
  -H "Authorization: Bearer $EZGH_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "base": "latest", "set": { "DB_PASSWORD": "n3w-s3cret" }, "remove": ["OLD_KEY"] }'
```

Send exactly one of `fields` or `base`. `base` is a version number or `"latest"`. Removing a field
the base doesn't have is `400`. `201` returns the new version's metadata:

```json
{
  "number": 3,
  "state": "enabled",
  "fieldNames": ["DB_PASSWORD", "DB_USER"],
  "fieldCount": 2,
  "createdBy": { "type": "Bot", "userName": "deployer" },
  "createdAt": "2026-10-02T09:58:02.114Z",
  "destroyedAt": null
}
```

(`createdBy` is the full Trails `userIdentity`.) Add `If-Match` with the secret's `ETag` to write
only if the secret hasn't changed (`412 precondition_failed` otherwise). A write that races
another for the same version number is `409 conflict`: run it again. At the enabled versions
quota, a write is `409 quota_exceeded`: destroy versions you no longer need.

## Read values

All the fields of a version:

```sh
curl "$BASE/secrets/$SECRET_ID/versions/latest/value" \
  -H "Authorization: Bearer $EZGH_API_KEY"
```

```json
{ "version": 3, "fields": { "DB_USER": "app", "DB_PASSWORD": "n3w-s3cret" } }
```

One field, at `.../versions/{version}/fields/{field}`, returns
`{ "version": 3, "name": "DB_PASSWORD", "value": "n3w-s3cret" }`. A text value is a string and a
binary value is `{ "base64": "..." }`.

Each of these calls is one billed read and is recorded in Trails. Read all the fields of a
version in one call rather than one call per field. A secret without an enabled version, an
unknown field or a destroyed version is `404`; for a destroyed version the error has
`error.details.reason: "destroyed"`.

## Destroy a version

```sh
curl -X POST "$BASE/secrets/$SECRET_ID/versions/2/destroy" \
  -H "Authorization: Bearer $EZGH_API_KEY"
```

returns `200` with the version, `state` `destroyed`. Destroying a version that's already
destroyed returns `200` with the version and changes nothing.

## Errors

Errors use the [standard error body](/api/errors). In addition to `401`, `403` and `404`:

| Status | `code` | When |
| --- | --- | --- |
| `400` | `invalid_request` | A bad name, field name, value encoding or `fields`/`base` combination. The `issues` name the path, such as `fields.DB_PASSWORD`, never the value. A value over 8 KiB is a `400` at its path. |
| `404` | `not_found` | Also a `base` that's already destroyed, with `error.details.reason: "destroyed"` |
| `409` | `secret_name_taken` | A secret with that name exists in the project |
| `409` | `conflict` | A concurrent write took the version number, or the `base` was destroyed meanwhile. Run the write again. |
| `409` | `organization_deletion_pending` | The organization is being deleted |
| `409` | `quota_exceeded` | A [quota](/secrets/pricing-and-limits#quotas) |
| `409` | `grant_exists`, `grants_unavailable` | See [Grants](/secrets/grants) |
| `412` | `precondition_failed` | `If-Match` doesn't match |
| `413` | `payload_too_large` | The body is over 16 MiB for creating a secret or a version, or over 1 MiB for any other request |
| `429` | `too_many_requests` | A [rate limit](/secrets/pricing-and-limits#rate-limits); wait for `Retry-After` |
| `503` | `unavailable` | An upstream service is unavailable; try again |

Source: https://docs.ezghcloud.com/secrets/api/index.mdx
