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.
Conventions
Section titled “Conventions”Authentication
Section titled “Authentication”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:
export VAULT_URL=https://vault.example.com:7999export VAULT_CA=/path/to/issuer.crt # the CA chain that signed Vault's leaf certexport VAULT_CERT=/path/to/your-client.crt # your client certexport VAULT_KEY=/path/to/your-client.key # your client cert's private keyThen each command starts:
curl --cacert $VAULT_CA --cert $VAULT_CERT --key $VAULT_KEY ...Content type
Section titled “Content type”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.
Method conventions
Section titled “Method conventions”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.
Common response shapes
Section titled “Common response shapes”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" }}Common HTTP status codes
Section titled “Common HTTP status codes”| Status | Meaning |
|---|---|
200 OK | Request succeeded. |
400 Bad Request | Invalid request body, missing required field, malformed JSON, or operation denied by access policy. Body contains the error code + detail. |
401 Unauthorized | mTLS handshake failed — your client cert is missing, expired, or not signed by the Vault service’s CA chain. |
404 Not Found | Resource absent (typically a secret-exists check). |
500 Internal Server Error | Backend 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.
Health endpoints (unauthenticated)
Section titled “Health endpoints (unauthenticated)”GET /health
Section titled “GET /health”Liveness probe — the process is running.
curl -sf $VAULT_URL/healthResponse: 200 OK with no body (or a small JSON status).
GET /ready
Section titled “GET /ready”Readiness probe — the process is ready to serve traffic. Returns
503 until the backend connection is established.
curl -sf $VAULT_URL/readyGET /version
Section titled “GET /version”Build identity. Useful for confirming you’re hitting the version you expect.
curl -s $VAULT_URL/versionResponse:
{ "version": "1.0.0", "commit": "abc123...", "buildtime": "2026-05-20T10:23:00Z" }Authentication exchange (mTLS → JWT)
Section titled “Authentication exchange (mTLS → JWT)”GET /certificate/auth
Section titled “GET /certificate/auth”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.
curl --cacert $VAULT_CA --cert $VAULT_CERT --key $VAULT_KEY \ $VAULT_URL/certificate/authResponse:
{ "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.
GET /certificate/internal/issuer
Section titled “GET /certificate/internal/issuer”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.
curl --cacert $VAULT_CA --cert $VAULT_CERT --key $VAULT_KEY \ $VAULT_URL/certificate/internal/issuerResponse:
{ "issuer": "-----BEGIN CERTIFICATE-----\nMIIDazCC...\n-----END CERTIFICATE-----\n" }Secrets
Section titled “Secrets”POST /secret/:name — save / overwrite
Section titled “POST /secret/:name — save / overwrite”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.
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_passwordRequest body:
| Field | Type | Required | Notes |
|---|---|---|---|
secret | string | yes | The secret value to store. |
datacenter | string | yes for tenant/cluster scope | dc accepted as alias. |
cluster | string | yes for tenant/cluster scope | |
customer | string | for tenant scope | All four (customer/product/environment/tenant) required together for tenant scope. |
product | string | for tenant scope | |
environment | string | for tenant scope | env accepted as alias. |
tenant | string | for tenant scope | |
startdate | RFC3339 timestamp | no | When the secret becomes active. Default: now. |
enddate | RFC3339 timestamp | no | When the secret expires. Default: 90 days from start unless indefinite: true. |
indefinite | bool | no | Skip 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.
PATCH /secret/:name — read
Section titled “PATCH /secret/:name — read”Reads the active secret matching the scope encoded in the request
body. PATCH (not GET) so the scope fits in a body.
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_passwordResponse:
{ "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.
PATCH /checksecret/:name — exists
Section titled “PATCH /checksecret/:name — exists”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).
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_passwordResponse:
{ "secretexists": true }Status codes:
200 OK— body indicates existence.404 Not Found— explicit “no such secret”.
DELETE /secret/:name — delete
Section titled “DELETE /secret/:name — delete”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).
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_passwordResponse:
{ "status": "success" }Certificates
Section titled “Certificates”The Vault service distinguishes four certificate flavors:
| Type | What | Issued by |
|---|---|---|
internal | mTLS identity for kis.ai services | The Vault service’s internal CA |
external | mTLS identity for TLS exposed to the public | external CA (uploaded) |
internaljwt | JWT signing keys for internal services | The Vault service’s internal CA |
externaljwt | External JWT verifier public keys | external 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.
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/internalRequest body:
| Field | Required | Notes |
|---|---|---|
datacenter, cluster, environment, service | yes | Scope. |
commonname | yes | Subject CN. |
dnsnames | yes | SAN DNS list. |
ips | yes | SAN IP list (strings). |
emailaddresses | no | SAN emails. |
uris | no | SAN URIs. The kis-conventional meta/?... URI is appended automatically. |
validitydays | no | Default 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.
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/internalPATCH /certificate/internal — retrieve
Section titled “PATCH /certificate/internal — retrieve”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.
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-----"}DELETE /certificate/internal — revoke
Section titled “DELETE /certificate/internal — revoke”Marks the matching cert(s) inactive. Same body shape as retrieve.
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"External certificate endpoints
Section titled “External certificate endpoints”Same shape as internal, with these path differences:
| Internal | External |
|---|---|
POST /certificate/internal | POST /certificate/external |
PATCH /certificate/internal | PATCH /certificate/external |
DELETE /certificate/internal | DELETE /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).
List endpoints
Section titled “List endpoints”PATCH /certs/internal/listPATCH /certs/external/listPATCH /certs/internaljwt/listPATCH /certs/externaljwt/listAll 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).
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/listdays filters to certs expiring within N days — useful for
rotation-due dashboards.
JWT signing material
Section titled “JWT signing material”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).
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/usersThe 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.
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.
Error code reference
Section titled “Error code reference”Common error codes that appear in the code field of error responses:
| Code | Meaning | Typical fix |
|---|---|---|
horus-invalid-payload | Malformed JSON or missing required field. | Inspect the data.error for which field. |
horus-invalid-secret-key | Secret name doesn’t match the expected pattern. | Check the URL path. |
horus-access-denied | Access policy refused the operation. | Verify your client cert’s claimed identity matches what the policy allows for this scope. |
horus-not-found | Resource doesn’t exist. | Verify the scope fields match what was saved. |
horus-max-active-certificate-limit-reached | Per-key active cert/secret cap hit (default 3). | Delete an inactive record first. |
horus-failed-to-retrieve-secret | Backend read failed. | Retry; if persistent, contact ops. |
horus-vault-not-connected | The Vault service has lost connection to its storage backend. | Retry; alarm if persistent. |
horus-missing-certificate | The request reached an endpoint that requires a client cert and none was extracted. | Confirm mTLS handshake completed (curl --cert / --key). |
Rate limiting
Section titled “Rate limiting”Not implemented in the current version. Production deployments may enforce limits via the surrounding load balancer; check with your ops team.
Versioning
Section titled “Versioning”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.
What’s coming (not yet shipping)
Section titled “What’s coming (not yet shipping)”The following are planned but not yet available on the wire:
| Endpoint / behavior | Planned phase |
|---|---|
| gRPC alternative on the same port | Phase 1 |
CSR-based cert issuance (POST /certificate/sign) | Phase 2 |
| CRL / OCSP for revocation | Phase 5 |
| Per-tenant envelope-encrypted at rest | Phase 5 |
Don’t build against any of these until they ship; the docs above are the contract.