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.
Lists are paged as described in Requests.
| Task | Operation |
|---|---|
| Create a secret | CreateSecret |
List secrets, or find one by name |
ListSecrets |
| Read a secret’s metadata | GetSecret |
| Change description and tags | UpdateSecret |
| Delete a secret | DeleteSecret |
| Write a version | CreateSecretVersion |
| List versions | ListSecretVersions |
| Read a version’s metadata | GetSecretVersion |
| Read a version’s values | AccessSecretVersion |
| Read one field | AccessSecretField |
| Destroy a version | DestroySecretVersion |
| Manage grants | CreateSecretGrant, ListSecretGrants, GetSecretGrant, DeleteSecretGrant |
Create a secret
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):
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:
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:
{
"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:
curl "$BASE/secrets/$SECRET_ID/versions/latest/value" \
-H "Authorization: Bearer $EZGH_API_KEY"{ "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
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. 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 |
409 |
grant_exists, grants_unavailable |
See 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; wait for Retry-After |
503 |
unavailable |
An upstream service is unavailable; try again |