Keycyte PAM 3.2 · Administrator Guide
Secrets API Reference
This reference describes the two endpoints of the Secrets API (version 1.0.0), the request signature, and the error responses. All requests go to your Keycyte PAM server over HTTPS, for example https://pam.example.com.
| Method | Path | Purpose |
|---|---|---|
GET | /api/secrets/accounts | List the accounts the key may read, without secret values. |
POST | /api/secrets/fetch | Read one secret of one account. |
Request Signature
Every request needs two headers, and both must be listed in the signature:
Date: Sat, 30 Aug 2026 17:04:47 GMT
Authorization: Signature keyId="<key-id>",algorithm="hmac-sha256",headers="(request-target) date",signature="<base64-signature>"
The signature is the Base64-encoded HMAC-SHA256 of the signing string, computed with the key secret. The signing string joins the listed headers with a newline (\n):
(request-target): post /api/secrets/fetch
date: Sat, 30 Aug 2026 17:04:47 GMT
- The method in
(request-target)is lowercase; the path is the full path, including any query string. - Only
hmac-sha256is accepted; RSA and ECDSA are rejected. Datemust be within about 300 seconds of the server time.
GET /api/secrets/accounts
Returns every account in the caller’s scope, sorted by name and then by accountId. Use it to look accounts up by name instead of hard-coding numeric IDs. The list is also the exact boundary of the key: it shows what a leak of the key would expose.
The response never contains password, privatekey or passphrase.
Response 200
| Field | Type | Description |
|---|---|---|
accounts | array | Accounts in scope (see below). |
count | integer | Number of entries in accounts. |
Each entry of accounts:
| Field | Type | Description |
|---|---|---|
accountId | integer | Numeric ID of the account. |
name | string | Account name. Pass it as name to /api/secrets/fetch. |
username | string | Login name of the account. Not treated as a secret. |
domain | string | Domain of the account. Omitted when empty. |
type | string | Account type, for example linux, windows, mysql. |
description | string | Administrator’s note. Omitted when empty. |
An empty list is a normal result: the identity holds no password roles, or none of them is inside its schedule window now.
{
"accounts": [
{ "accountId": 6365, "name": "app-db", "username": "appuser", "type": "mysql" }
],
"count": 1
}
POST /api/secrets/fetch
Returns one secret value of one account. The account is identified in the request body, not in the URL, so that it does not appear in access logs.
Request body
Send exactly one of accountId or name.
| Field | Type | Description |
|---|---|---|
accountId | integer | Numeric ID of the account. Stable for the life of the account, but changes if the account is deleted and created again. |
name | string | Account name. Matched case-insensitively, with surrounding spaces ignored, and only within the caller’s scope. |
field | string | Secret to read: password (default), privatekey or passphrase. For example, an API key is stored in password and a TOTP seed in privatekey. |
reason | string | Optional text recorded in the audit log, up to 256 bytes. It never changes the result. |
Response 200
| Field | Type | Description |
|---|---|---|
accountId | integer | Account that was read. Check it when you asked by name. |
field | string | Field that was returned, in lowercase. |
value | string | The secret, in clear text. |
Warning
Treat the response as sensitive. Do not log it, cache it on disk, or include it in error reports.
Error Responses
| Status | Body | Meaning |
|---|---|---|
400 | {"message": "..."} | Neither or both of accountId and name were sent, field is not one of the three names, or the requested field is empty on the account. |
401 | {"error": "unauthorized"} | The signature is missing, malformed, expired, or does not match an active key. The body does not say which; the server log holds the reason. |
403 | {"error": "forbidden"} or {"message": "this credential cannot read secrets"} | The call came from an IP address outside the key’s allow-list, or the key has no owning user. |
404 | {"message": "secret not found"} | No secret was returned. The same response covers a missing account, an account outside the caller’s scope, and a name that matches nothing the caller can see. |
409 with Retry-After | {"message": "account password is being rotated: ..."} | The account’s password is being changed now. Wait the number of seconds in Retry-After and retry. If the condition persists for several minutes, the rotation could not save the new password and an administrator must resolve it; raise an alarm instead of retrying forever. |
409 without Retry-After | {"message": "account name is ambiguous: ..."} | name matches more than one account in the caller’s scope. Use accountId, or rename one of the accounts. |
500 | {"message": "..."} | Server error. |
Note
Errors from the authentication layer (401, and 403 for IP restrictions) use the error field; errors from the endpoints use the message field. Read both in your client.
Example
The following shell script reads the password of the account named app-db. Replace the placeholders with your values.
KEY_ID="<key-id>"
KEY_SECRET="<key-secret>"
HOST="pam.example.com"
PATH_="/api/secrets/fetch"
DATE="$(LC_ALL=C date -u '+%a, %d %b %Y %H:%M:%S GMT')"
SIGNING_STRING="$(printf '(request-target): post %s\ndate: %s' "$PATH_" "$DATE")"
SIGNATURE="$(printf '%s' "$SIGNING_STRING" | openssl dgst -sha256 -hmac "$KEY_SECRET" -binary | base64)"
curl -s "https://$HOST$PATH_" \
-H "Date: $DATE" \
-H "Authorization: Signature keyId=\"$KEY_ID\",algorithm=\"hmac-sha256\",headers=\"(request-target) date\",signature=\"$SIGNATURE\"" \
-H "Content-Type: application/json" \
-d '{"name": "app-db", "field": "password", "reason": "nightly backup job"}'
A successful call returns:
{ "accountId": 6365, "field": "password", "value": "<secret-value>" }
The HMAC key is the key secret exactly as it was shown when the key was issued (used as a text string, not decoded).