Skip to content
Talk to our solutions team

Vault API

Reference for the vault.svc HTTP surface. For concepts (custody, envelope encryption, caller-scope) see the Vault block docs; for config see Vault Configuration.

Wire-level reference for every REST endpoint vault.svc exposes. Every request example uses curl with the mTLS client certificate your ops team issued you.

All endpoints below (except /health, /ready, /version) require mTLS. Your client must present a certificate that the Vault service’s root CA chain validates. Plain HTTPS without a client cert returns 401 Unauthorized at the TLS layer (handshake fails before the request reaches a handler).

For convenience, every curl example below uses these env vars:

Terminal window
export VAULT_URL=https://vault.example.com:7999
export VAULT_CA=/path/to/issuer.crt # the CA chain that signed Vault's leaf cert
export VAULT_CERT=/path/to/your-client.crt # your client cert
export VAULT_KEY=/path/to/your-client.key # your client cert's private key

Then each command starts:

Terminal window
curl --cacert $VAULT_CA --cert $VAULT_CERT --key $VAULT_KEY ...

Request and response bodies are application/json. The Vault service accepts requests without an explicit Content-Type header for compatibility, but you should send Content-Type: application/json to be explicit.

The Vault service uses PATCH for reads that carry a request body (the scope parameters that determine which secret/cert to fetch). This is non-standard REST but consistent across the surface. Treat any PATCH here as a “read-with-body” — it does not mutate state.

Success — varies per endpoint, documented below.

Error — always the same shape (the horus- prefix on code is the Vault service’s internal codename):

{
"code": "horus-<error-code>",
"message": "human-readable summary",
"data": {
"error": "more-specific detail"
}
}
StatusMeaning
200 OKRequest succeeded.
400 Bad RequestInvalid request body, missing required field, malformed JSON, or operation denied by access policy. Body contains the error code + detail.
401 UnauthorizedmTLS handshake failed — your client cert is missing, expired, or not signed by the Vault service’s CA chain.
404 Not FoundResource absent (typically a secret-exists check).
500 Internal Server ErrorBackend or signer failure. Retry with backoff; if it persists, contact your ops team.

There is no 403 Forbidden — policy denials surface as 400 with an access-denied code in the body.

Liveness probe — the process is running.

Terminal window
curl -sf $VAULT_URL/health

Response: 200 OK with no body (or a small JSON status).

Readiness probe — the process is ready to serve traffic. Returns 503 until the backend connection is established.

Terminal window
curl -sf $VAULT_URL/ready

Build identity. Useful for confirming you’re hitting the version you expect.

Terminal window
curl -s $VAULT_URL/version

Response:

{ "version": "1.0.0", "commit": "abc123...", "buildtime": "2026-05-20T10:23:00Z" }

Exchange your mTLS client cert for a short-lived service JWT. The JWT is what you attach to outbound calls to other services that require Bearer auth.

Terminal window
curl --cacert $VAULT_CA --cert $VAULT_CERT --key $VAULT_KEY \
$VAULT_URL/certificate/auth

Response:

{ "token": "eyJhbGciOiJSUzI1NiIs..." }

JWT lifetime: 1 hour by default (configurable per cluster via servicejwt.expiry). The JWT’s sub and iss claims are derived from your client certificate’s URI SAN — you cannot mint a JWT for an identity other than the one your cert represents.

Fetch the public CA chain (the “issuer bundle”) the Vault service in your cluster uses to sign certificates. Use this to verify certs you receive from peer services.

Terminal window
curl --cacert $VAULT_CA --cert $VAULT_CERT --key $VAULT_KEY \
$VAULT_URL/certificate/internal/issuer

Response:

{ "issuer": "-----BEGIN CERTIFICATE-----\nMIIDazCC...\n-----END CERTIFICATE-----\n" }

Stores a secret under the scope encoded in the request body. The request’s CPET fields (customer/product/environment/tenant) determine the scope. Omit them to write to cluster scope; omit dc/cluster too to write to global scope.

Terminal window
curl --cacert $VAULT_CA --cert $VAULT_CERT --key $VAULT_KEY \
-X POST \
-H "Content-Type: application/json" \
-d '{
"secret": "hunter2",
"datacenter": "us-west",
"cluster": "prod-cluster-01",
"customer": "acme",
"product": "forge",
"environment":"prod",
"tenant": "acme-prod",
"indefinite": true
}' \
$VAULT_URL/secret/db_password

Request body:

FieldTypeRequiredNotes
secretstringyesThe secret value to store.
datacenterstringyes for tenant/cluster scopedc accepted as alias.
clusterstringyes for tenant/cluster scope
customerstringfor tenant scopeAll four (customer/product/environment/tenant) required together for tenant scope.
productstringfor tenant scope
environmentstringfor tenant scopeenv accepted as alias.
tenantstringfor tenant scope
startdateRFC3339 timestampnoWhen the secret becomes active. Default: now.
enddateRFC3339 timestampnoWhen the secret expires. Default: 90 days from start unless indefinite: true.
indefiniteboolnoSkip enddate; secret has no expiry. Use sparingly.

Response:

{ "status": "success" }

Status codes:

  • 200 OK — saved.
  • 400 Bad Request — invalid payload, missing required scope field, or denied by access policy.

Reads the active secret matching the scope encoded in the request body. PATCH (not GET) so the scope fits in a body.

Terminal window
curl --cacert $VAULT_CA --cert $VAULT_CERT --key $VAULT_KEY \
-X PATCH \
-H "Content-Type: application/json" \
-d '{
"datacenter": "us-west",
"cluster": "prod-cluster-01",
"customer": "acme",
"product": "forge",
"environment":"prod",
"tenant": "acme-prod"
}' \
$VAULT_URL/secret/db_password

Response:

{
"startdate": "2026-05-21T08:00:00Z",
"enddate": "2026-08-19T08:00:00Z",
"secret": "hunter2"
}

Status codes:

  • 200 OK — secret returned.
  • 400 Bad Request — payload invalid or access denied.

Same scope shape as the read. Returns whether a matching active secret exists, without returning its value. Useful as a guard before calling POST (to decide whether to insert vs. roll the value).

Terminal window
curl --cacert $VAULT_CA --cert $VAULT_CERT --key $VAULT_KEY \
-X PATCH \
-H "Content-Type: application/json" \
-d '{ "datacenter":"us-west", "cluster":"prod-cluster-01" }' \
$VAULT_URL/checksecret/db_password

Response:

{ "secretexists": true }

Status codes:

  • 200 OK — body indicates existence.
  • 404 Not Found — explicit “no such secret”.

Same scope as read; additionally requires the id field of the specific record you want to delete (each save creates a versioned entity record; you delete a specific version’s record).

Terminal window
curl --cacert $VAULT_CA --cert $VAULT_CERT --key $VAULT_KEY \
-X DELETE \
-H "Content-Type: application/json" \
-d '{
"id": "01HXYZ...",
"datacenter": "us-west",
"cluster": "prod-cluster-01",
"customer": "acme",
"product": "forge",
"environment":"prod",
"tenant": "acme-prod"
}' \
$VAULT_URL/secret/db_password

Response:

{ "status": "success" }

The Vault service distinguishes four certificate flavors:

TypeWhatIssued by
internalmTLS identity for kis.ai servicesThe Vault service’s internal CA
externalmTLS identity for TLS exposed to the publicexternal CA (uploaded)
internaljwtJWT signing keys for internal servicesThe Vault service’s internal CA
externaljwtExternal JWT verifier public keysexternal authority (uploaded)

POST /certificate/generate/internal — issue a new internal service cert

Section titled “POST /certificate/generate/internal — issue a new internal service cert”

Generates an RSA-4096 keypair, signs the cert with the Vault service’s internal CA, stores it, and returns the certificate serial. You’d then read the cert+key separately via the retrieve endpoint.

Terminal window
curl --cacert $VAULT_CA --cert $VAULT_CERT --key $VAULT_KEY \
-X POST \
-H "Content-Type: application/json" \
-d '{
"datacenter": "us-west",
"cluster": "prod-cluster-01",
"environment": "prod",
"service": "themis",
"commonname": "themis.us-west.prod.kis.ai",
"dnsnames": ["themis.us-west.prod.kis.ai", "localhost"],
"ips": ["10.0.1.5"],
"emailaddresses": ["[email protected]"],
"validitydays": 90
}' \
$VAULT_URL/certificate/generate/internal

Request body:

FieldRequiredNotes
datacenter, cluster, environment, serviceyesScope.
commonnameyesSubject CN.
dnsnamesyesSAN DNS list.
ipsyesSAN IP list (strings).
emailaddressesnoSAN emails.
urisnoSAN URIs. The kis-conventional meta/?... URI is appended automatically.
validitydaysnoDefault 90.

Response:

{ "certificateserial": 1716279600123 }

POST /certificate/internal — save an externally-generated cert

Section titled “POST /certificate/internal — save an externally-generated cert”

If you have a cert+key pair generated outside the Vault service, store them. The cert key and body are sent in the payload alongside the scope.

Terminal window
curl --cacert $VAULT_CA --cert $VAULT_CERT --key $VAULT_KEY \
-X POST \
-H "Content-Type: application/json" \
-d '{
"datacenter": "us-west",
"cluster": "prod-cluster-01",
"environment": "prod",
"service": "themis",
"cn": "themis.us-west.prod.kis.ai",
"cert": "-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----",
"key": "-----BEGIN RSA PRIVATE KEY-----\n...\n-----END RSA PRIVATE KEY-----"
}' \
$VAULT_URL/certificate/internal

Returns the cert and (if policy allows) the private key for the identified service. Whether the key is returned is governed by the cluster’s access policy.

Terminal window
curl --cacert $VAULT_CA --cert $VAULT_CERT --key $VAULT_KEY \
-X PATCH \
-H "Content-Type: application/json" \
-d '{
"datacenter": "us-west",
"cluster": "prod-cluster-01",
"environment": "prod",
"service": "themis",
"commonname": "themis.us-west.prod.kis.ai"
}' \
"$VAULT_URL/certificate/internal?serial=1716279600123"

Omit ?serial=... to get the currently active cert for the identity.

Response:

{
"cert": "-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----",
"key": "-----BEGIN RSA PRIVATE KEY-----\n...\n-----END RSA PRIVATE KEY-----"
}

Marks the matching cert(s) inactive. Same body shape as retrieve.

Terminal window
curl --cacert $VAULT_CA --cert $VAULT_CERT --key $VAULT_KEY \
-X DELETE \
-H "Content-Type: application/json" \
-d '{
"datacenter": "us-west",
"cluster": "prod-cluster-01",
"environment": "prod",
"service": "themis"
}' \
"$VAULT_URL/certificate/internal?serial=1716279600123"

Same shape as internal, with these path differences:

InternalExternal
POST /certificate/internalPOST /certificate/external
PATCH /certificate/internalPATCH /certificate/external
DELETE /certificate/internalDELETE /certificate/external

External endpoints require an additional authority field in the body identifying the issuing CA, plus an issuer field on save (the CA’s own cert PEM).

PATCH /certs/internal/list
PATCH /certs/external/list
PATCH /certs/internaljwt/list
PATCH /certs/externaljwt/list

All take the same body shape: a subset of scope fields plus optional days, fields, authority. The response is a list of matching certificate metadata records (no cert bodies, no private keys).

Terminal window
curl --cacert $VAULT_CA --cert $VAULT_CERT --key $VAULT_KEY \
-X PATCH \
-H "Content-Type: application/json" \
-d '{
"datacenter": "us-west",
"cluster": "prod-cluster-01",
"days": 30
}' \
$VAULT_URL/certs/internal/list

days filters to certs expiring within N days — useful for rotation-due dashboards.

POST /jwt/internal/generate/:identifier — mint a per-realm JWT signing cert

Section titled “POST /jwt/internal/generate/:identifier — mint a per-realm JWT signing cert”

Used by services that issue their own JWTs (e.g., a federated auth service issuing tokens for a tenant realm).

Terminal window
curl --cacert $VAULT_CA --cert $VAULT_CERT --key $VAULT_KEY \
-X POST \
-H "Content-Type: application/json" \
-d '{
"datacenter": "us-west",
"cluster": "prod-cluster-01",
"environment": "prod",
"service": "the platform identity service",
"customer": "acme",
"product": "forge",
"tenant": "acme-prod"
}' \
$VAULT_URL/jwt/internal/generate/users

The path’s :identifier is the realm name (here, users). Returns the cert serial; use PATCH /jwt/internal/:identifier to retrieve the cert + public key.

POST /jwt/internal/generate — mint the Vault service’s own JWT cert

Section titled “POST /jwt/internal/generate — mint the Vault service’s own JWT cert”

Used by the Vault service itself to sign service JWTs (the ones returned from /certificate/auth). You rarely call this directly — your ops team or vault.svc bootstrap takes care of it.

PATCH /jwt/internal/:identifier — retrieve a per-realm JWT cert + public key

Section titled “PATCH /jwt/internal/:identifier — retrieve a per-realm JWT cert + public key”

Used by services that need to verify JWTs minted by another service’s realm.

Terminal window
curl --cacert $VAULT_CA --cert $VAULT_CERT --key $VAULT_KEY \
-X PATCH \
-H "Content-Type: application/json" \
-d '{
"datacenter": "us-west",
"cluster": "prod-cluster-01",
"environment": "prod",
"service": "the platform identity service",
"customer": "acme",
"product": "forge",
"tenant": "acme-prod"
}' \
"$VAULT_URL/jwt/internal/users?serial=1716279600123"

Response:

{
"cert": "-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----",
"publickey": "-----BEGIN PUBLIC KEY-----\n...\n-----END PUBLIC KEY-----",
"key": "-----BEGIN RSA PRIVATE KEY-----\n...\n-----END RSA PRIVATE KEY-----"
}

The key field is returned only if your client identity is the owning service. Verifiers get just cert + publickey.

PATCH /jwt/internal — retrieve the Vault service’s own JWT cert

Section titled “PATCH /jwt/internal — retrieve the Vault service’s own JWT cert”

Used to verify JWTs the Vault service itself minted (typically the ones returned by /certificate/auth).

POST /jwt/external/:identifier — upload an external JWT verifier public key

Section titled “POST /jwt/external/:identifier — upload an external JWT verifier public key”

For verifying JWTs issued by an external authority (e.g., your customer’s OIDC provider).

PATCH /jwt/external/:identifier — retrieve an external JWT verifier public key

Section titled “PATCH /jwt/external/:identifier — retrieve an external JWT verifier public key”

Same shape as the internal variant; requires an authority field in the body identifying the external issuer.

Common error codes that appear in the code field of error responses:

CodeMeaningTypical fix
horus-invalid-payloadMalformed JSON or missing required field.Inspect the data.error for which field.
horus-invalid-secret-keySecret name doesn’t match the expected pattern.Check the URL path.
horus-access-deniedAccess policy refused the operation.Verify your client cert’s claimed identity matches what the policy allows for this scope.
horus-not-foundResource doesn’t exist.Verify the scope fields match what was saved.
horus-max-active-certificate-limit-reachedPer-key active cert/secret cap hit (default 3).Delete an inactive record first.
horus-failed-to-retrieve-secretBackend read failed.Retry; if persistent, contact ops.
horus-vault-not-connectedThe Vault service has lost connection to its storage backend.Retry; alarm if persistent.
horus-missing-certificateThe request reached an endpoint that requires a client cert and none was extracted.Confirm mTLS handshake completed (curl --cert / --key).

Not implemented in the current version. Production deployments may enforce limits via the surrounding load balancer; check with your ops team.

The Vault service uses semantic versioning. Within a major version, the wire protocol is stable: existing endpoints don’t change shape, only new endpoints get added. Breaking changes go into a major version bump and are documented in release notes. Track the version your env runs via GET /version.

The following are planned but not yet available on the wire:

Endpoint / behaviorPlanned phase
gRPC alternative on the same portPhase 1
CSR-based cert issuance (POST /certificate/sign)Phase 2
CRL / OCSP for revocationPhase 5
Per-tenant envelope-encrypted at restPhase 5

Don’t build against any of these until they ship; the docs above are the contract.