Key Management Service (KMS)
Securely orchestrate sensitive credentials, enforce network-level access controls, and maintain comprehensive audit trails for cryptographic secrets.
Key Management Service (KMS) API
The Key Management Service (KMS) provides a hardened environment for the storage and lifecycle management of sensitive credentials, such as third-party API keys, service accounts, and cryptographic tokens. By using KMS, organizations can eliminate the risk of hardcoded secrets within source code, instead retrieving them dynamically via secure, audited endpoints protected by strict IP-based whitelisting.
Authenticate with Authorization: Bearer <API_KEY>. Inventory, configuration and reveal require kms:read; creation and modification require kms:write. The company always comes from the authenticated API key. Both 2026-02-01 and 2024-08-01 support these endpoints.
Secret Management
Create a Secret
Send Content-Type: application/json and a body containing only the KMSEntry object, up to 64 KiB. name, handler and content are required non-empty strings. Names and handlers accept up to 255 UTF-8 bytes. Handlers are normalized to lowercase slugs (for example, Production Service becomes production-service); use the returned handler in later requests. Content is encrypted before storage and never returned by write endpoints.
kms:writePath, query, and header parameters
No path, query, or additional header parameters.
Request Body
{
"KMSEntry": {
"name": "Production service token",
"handler": "production-service",
"content": "EXAMPLE_SECRET_REDACTED",
"whitelist": [
"203.0.113.10"
],
"properties": {
"environment": "production"
}
}
}KMSEntryRequiredKMSEntry.nameRequiredKMSEntry.handlerRequiredKMSEntry.contentRequiredKMSEntry.whitelistKMSEntry.propertiesResponses
Secret created; response contains metadata only.
{
"id": "6835c2cd...",
"name": "Production service token",
"handler": "production-service",
"whitelist": [
"203.0.113.10"
],
"properties": {
"environment": "production"
},
"revealable": true
}The normalized handler already exists in this company.
{
"error": "A KMS Entry with this handler already exists"
}Request examples
curl --request POST 'https://api.hub.donutwork.com/2026-02-01/kms.json' \
--header 'Authorization: Bearer YOUR_API_KEY' \
--header 'Content-Type: application/json' \
--data '{
"KMSEntry": {
"name": "Production service token",
"handler": "production-service",
"content": "EXAMPLE_SECRET_REDACTED",
"whitelist": [
"203.0.113.10"
],
"properties": {
"environment": "production"
}
}
}'const response = await fetch('https://api.hub.donutwork.com/2026-02-01/kms.json', {
method: 'POST',
headers: {
Authorization: 'Bearer ' + process.env.DONUTWORK_API_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({
"KMSEntry": {
"name": "Production service token",
"handler": "production-service",
"content": "EXAMPLE_SECRET_REDACTED",
"whitelist": [
"203.0.113.10"
],
"properties": {
"environment": "production"
}
}
}),
});
const requestId = response.headers.get('x-request-id');
if (!response.ok) {
const error = await response.text();
throw new Error(`${response.status} ${error} [${requestId}]`);
}
const contentType = response.headers.get('content-type') ?? '';
const data = response.status === 204 ? null
: contentType.includes('application/json') ? await response.json()
: await response.text();$request = curl_init('https://api.hub.donutwork.com/2026-02-01/kms.json');
curl_setopt_array($request, [
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . getenv('DONUTWORK_API_KEY'),
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => '{
"KMSEntry": {
"name": "Production service token",
"handler": "production-service",
"content": "EXAMPLE_SECRET_REDACTED",
"whitelist": [
"203.0.113.10"
],
"properties": {
"environment": "production"
}
}
}',
]);
$response = curl_exec($request);
if ($response === false) {
$error = curl_error($request);
curl_close($request);
throw new RuntimeException($error);
}
$status = curl_getinfo($request, CURLINFO_RESPONSE_CODE);
$contentType = (string) curl_getinfo($request, CURLINFO_CONTENT_TYPE);
curl_close($request);
if ($status >= 400) { throw new RuntimeException($response); }
$data = $status === 204 ? null
: (str_contains($contentType, 'application/json')
? json_decode($response, true, flags: JSON_THROW_ON_ERROR) : $response);Replace uppercase placeholders and sample values with your own data and company resource IDs. Keep the API key on a trusted server. Reuse an idempotency key only for the same logical request and payload.
Update Configuration or Rotate a Secret
Use the current handler in the URL and send at least one field inside KMSEntry. Omitted fields keep their current values, including encrypted content. To rotate a secret, send content; to rename its alias, send handler and use the returned slug afterward. A supplied whitelist or properties replaces the entire corresponding collection; use [] or {} to clear it.
Ownership cannot be changed: company_id and companyId are rejected with 400, whether supplied inside KMSEntry or at the body root. Lookup and update are scoped to the authenticated company; a handler belonging only to another company returns 404. A company_id key nested inside properties is metadata only and has no effect on ownership.
kms:writePath, query, and header parameters
handlerRequiredRequest Body
{
"KMSEntry": {
"content": "EXAMPLE_ROTATED_SECRET_REDACTED"
}
}KMSEntryRequiredKMSEntry.nameKMSEntry.handlerKMSEntry.contentKMSEntry.whitelistKMSEntry.propertiesResponses
Secret updated; response contains metadata only.
{
"id": "6835c2cd...",
"name": "Production service token",
"handler": "production-service",
"whitelist": [
"203.0.113.10"
],
"properties": {
"environment": "production"
},
"revealable": true
}Non-revealable entries cannot be modified through this API.
{
"error": "This KMS Entry cannot be edited"
}The handler does not exist in the authenticated company.
{
"error": "This KMSEntry does not exist"
}The new normalized handler belongs to another entry.
{
"error": "A KMS Entry with this handler already exists"
}Request examples
curl --request PUT 'https://api.hub.donutwork.com/2026-02-01/kms/HANDLER.json' \
--header 'Authorization: Bearer YOUR_API_KEY' \
--header 'Content-Type: application/json' \
--data '{
"KMSEntry": {
"content": "EXAMPLE_ROTATED_SECRET_REDACTED"
}
}'const response = await fetch('https://api.hub.donutwork.com/2026-02-01/kms/HANDLER.json', {
method: 'PUT',
headers: {
Authorization: 'Bearer ' + process.env.DONUTWORK_API_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({
"KMSEntry": {
"content": "EXAMPLE_ROTATED_SECRET_REDACTED"
}
}),
});
const requestId = response.headers.get('x-request-id');
if (!response.ok) {
const error = await response.text();
throw new Error(`${response.status} ${error} [${requestId}]`);
}
const contentType = response.headers.get('content-type') ?? '';
const data = response.status === 204 ? null
: contentType.includes('application/json') ? await response.json()
: await response.text();$request = curl_init('https://api.hub.donutwork.com/2026-02-01/kms/HANDLER.json');
curl_setopt_array($request, [
CURLOPT_CUSTOMREQUEST => 'PUT',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . getenv('DONUTWORK_API_KEY'),
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => '{
"KMSEntry": {
"content": "EXAMPLE_ROTATED_SECRET_REDACTED"
}
}',
]);
$response = curl_exec($request);
if ($response === false) {
$error = curl_error($request);
curl_close($request);
throw new RuntimeException($error);
}
$status = curl_getinfo($request, CURLINFO_RESPONSE_CODE);
$contentType = (string) curl_getinfo($request, CURLINFO_CONTENT_TYPE);
curl_close($request);
if ($status >= 400) { throw new RuntimeException($response); }
$data = $status === 204 ? null
: (str_contains($contentType, 'application/json')
? json_decode($response, true, flags: JSON_THROW_ON_ERROR) : $response);Replace uppercase placeholders and sample values with your own data and company resource IDs. Keep the API key on a trusted server. Reuse an idempotency key only for the same logical request and payload.
Write errors: 400 for invalid JSON, missing/unknown fields or invalid types/IPs; 401 for invalid authentication; 403 for insufficient scope or protected credentials; 413 for a body over 64 KiB; 415 for a content type other than JSON; 500 for a storage/encryption failure.
Entries with properties.revealable=false cannot be revealed or edited through the public API. Setting this flag is irreversible through these endpoints. AI provider credentials also remain protected and must use their dedicated APIs. Keep secrets in content: properties is readable metadata.
Creation and modification generate audit records containing field names only. Request and response bodies for writes are excluded from API content logging, and write responses use Cache-Control: no-store. The IP whitelist controls reveal, while writes require kms:write independently of that whitelist.
These endpoints do not accept an idempotency key. A repeated POST with an existing handler returns 409; after an uncertain response, retrieve its configuration with kms:read before retrying. Serialize creation and renaming for the same handler: collision checks follow the existing KMS storage behavior and do not provide a concurrency guarantee.
Audit KMS Inventory
Retrieve a paginated directory of all KMS secret handlers associated with your organization. This endpoint provides administrative metadata and is used for secret discovery; it does not expose the underlying secret values.
kms:readPath, query, and header parameters
sizepageResponses
Secret inventory successfully retrieved.
{
"entities": "KMSEntry",
"count": 12,
"per_page": 100,
"pages": {
"current": 1,
"max": 1
},
"elements": [
{
"id": "6835c2cd...",
"name": "Production AWS S3 Access",
"handler": "aws-s3-prod-credentials",
"revealable": true
}
]
}Request examples
curl --request GET 'https://api.hub.donutwork.com/2026-02-01/kms.json' \
--header 'Authorization: Bearer YOUR_API_KEY'const response = await fetch('https://api.hub.donutwork.com/2026-02-01/kms.json', {
method: 'GET',
headers: {
Authorization: 'Bearer ' + process.env.DONUTWORK_API_KEY,
},
});
const requestId = response.headers.get('x-request-id');
if (!response.ok) {
const error = await response.text();
throw new Error(`${response.status} ${error} [${requestId}]`);
}
const contentType = response.headers.get('content-type') ?? '';
const data = response.status === 204 ? null
: contentType.includes('application/json') ? await response.json()
: await response.text();$request = curl_init('https://api.hub.donutwork.com/2026-02-01/kms.json');
curl_setopt_array($request, [
CURLOPT_CUSTOMREQUEST => 'GET',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . getenv('DONUTWORK_API_KEY'),
],
]);
$response = curl_exec($request);
if ($response === false) {
$error = curl_error($request);
curl_close($request);
throw new RuntimeException($error);
}
$status = curl_getinfo($request, CURLINFO_RESPONSE_CODE);
$contentType = (string) curl_getinfo($request, CURLINFO_CONTENT_TYPE);
curl_close($request);
if ($status >= 400) { throw new RuntimeException($response); }
$data = $status === 204 ? null
: (str_contains($contentType, 'application/json')
? json_decode($response, true, flags: JSON_THROW_ON_ERROR) : $response);Replace uppercase placeholders and sample values with your own data and company resource IDs. Keep the API key on a trusted server. Reuse an idempotency key only for the same logical request and payload.
Retrieve Secret Configuration
Fetch the detailed metadata and operational configuration for a unique KMS handler. This includes the registered IP whitelist and custom properties defined for the secret.
kms:readPath, query, and header parameters
handlerRequiredResponses
Secret configuration retrieved.
{
"id": "6835c2cd...",
"name": "Production AWS S3 Access",
"handler": "aws-s3-prod-credentials",
"whitelist": [
"52.1.2.3",
"54.0.0.1"
],
"properties": {
"region": "us-east-1"
},
"revealable": true
}The specified handler does not exist in the vault.
{
"error": "This KMSEntry does not exist"
}Request examples
curl --request GET 'https://api.hub.donutwork.com/2026-02-01/kms/HANDLER.json' \
--header 'Authorization: Bearer YOUR_API_KEY'const response = await fetch('https://api.hub.donutwork.com/2026-02-01/kms/HANDLER.json', {
method: 'GET',
headers: {
Authorization: 'Bearer ' + process.env.DONUTWORK_API_KEY,
},
});
const requestId = response.headers.get('x-request-id');
if (!response.ok) {
const error = await response.text();
throw new Error(`${response.status} ${error} [${requestId}]`);
}
const contentType = response.headers.get('content-type') ?? '';
const data = response.status === 204 ? null
: contentType.includes('application/json') ? await response.json()
: await response.text();$request = curl_init('https://api.hub.donutwork.com/2026-02-01/kms/HANDLER.json');
curl_setopt_array($request, [
CURLOPT_CUSTOMREQUEST => 'GET',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . getenv('DONUTWORK_API_KEY'),
],
]);
$response = curl_exec($request);
if ($response === false) {
$error = curl_error($request);
curl_close($request);
throw new RuntimeException($error);
}
$status = curl_getinfo($request, CURLINFO_RESPONSE_CODE);
$contentType = (string) curl_getinfo($request, CURLINFO_CONTENT_TYPE);
curl_close($request);
if ($status >= 400) { throw new RuntimeException($response); }
$data = $status === 204 ? null
: (str_contains($contentType, 'application/json')
? json_decode($response, true, flags: JSON_THROW_ON_ERROR) : $response);Replace uppercase placeholders and sample values with your own data and company resource IDs. Keep the API key on a trusted server. Reuse an idempotency key only for the same logical request and payload.
Secret Consumption
Decrypt & Reveal Secret
Securely decrypt and reveal the underlying secret content. A non-empty whitelist requires an exact requesting IP match; an empty whitelist imposes no IP restriction. Non-revealable entries and AI provider credentials always return 403. Reveal access is audited. Use this endpoint only within backend environments to dynamically inject credentials.
kms:readPath, query, and header parameters
handlerRequiredResponses
Secret successfully decrypted and returned.
{
"content": "EXAMPLE_ACCESS_KEY_REDACTED"
}Requesting IP is not in the authorized whitelist or permissions are insufficient.
{
"error": "Unauthorized to reveal this KMS Entry"
}Request examples
curl --request GET 'https://api.hub.donutwork.com/2026-02-01/kms/reveal/HANDLER.json' \
--header 'Authorization: Bearer YOUR_API_KEY'const response = await fetch('https://api.hub.donutwork.com/2026-02-01/kms/reveal/HANDLER.json', {
method: 'GET',
headers: {
Authorization: 'Bearer ' + process.env.DONUTWORK_API_KEY,
},
});
const requestId = response.headers.get('x-request-id');
if (!response.ok) {
const error = await response.text();
throw new Error(`${response.status} ${error} [${requestId}]`);
}
const contentType = response.headers.get('content-type') ?? '';
const data = response.status === 204 ? null
: contentType.includes('application/json') ? await response.json()
: await response.text();$request = curl_init('https://api.hub.donutwork.com/2026-02-01/kms/reveal/HANDLER.json');
curl_setopt_array($request, [
CURLOPT_CUSTOMREQUEST => 'GET',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . getenv('DONUTWORK_API_KEY'),
],
]);
$response = curl_exec($request);
if ($response === false) {
$error = curl_error($request);
curl_close($request);
throw new RuntimeException($error);
}
$status = curl_getinfo($request, CURLINFO_RESPONSE_CODE);
$contentType = (string) curl_getinfo($request, CURLINFO_CONTENT_TYPE);
curl_close($request);
if ($status >= 400) { throw new RuntimeException($response); }
$data = $status === 204 ? null
: (str_contains($contentType, 'application/json')
? json_decode($response, true, flags: JSON_THROW_ON_ERROR) : $response);Replace uppercase placeholders and sample values with your own data and company resource IDs. Keep the API key on a trusted server. Reuse an idempotency key only for the same logical request and payload.
Technical Implementation
Security Audit: Every call to the reveal endpoint is logged with the requesting IP, User-Agent, and timestamp. Ensure your infrastructure IPs are properly whitelisted in the KMS configuration to avoid 403 authorization failures.