DonutworkHelp Center
Api ReferenceSystems & Monitoring
Donutwork documentation

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.

POST
/2026-02-01/kms.json
Required permissionkms:write

Path, query, and header parameters

No path, query, or additional header parameters.

Request Body

JSON
{
  "KMSEntry": {
    "name": "Production service token",
    "handler": "production-service",
    "content": "EXAMPLE_SECRET_REDACTED",
    "whitelist": [
      "203.0.113.10"
    ],
    "properties": {
      "environment": "production"
    }
  }
}
KMSEntryobjectRequired
Secret fields. Unknown fields, including company_id, are rejected.
KMSEntry.namestringRequired
Display name, up to 255 UTF-8 bytes.
KMSEntry.handlerstringRequired
Alias normalized to a non-empty slug; an existing alias in this company returns 409.
KMSEntry.contentstringRequired
Non-empty secret value. Whitespace is preserved; the value is encrypted at rest.
KMSEntry.whitelistarray
Exact IPv4/IPv6 addresses, without CIDR. Defaults to []; an empty list imposes no IP restriction on reveal.
KMSEntry.propertiesobject
Public metadata only. revealable must be boolean; ai_provider_credential is reserved for the dedicated AI APIs.

Responses

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.

PUT
/2026-02-01/kms/{handler}.json
Required permissionkms:write

Path, query, and header parameters

handlerstring· pathRequired
Current handler in the authenticated company.

Request Body

JSON
{
  "KMSEntry": {
    "content": "EXAMPLE_ROTATED_SECRET_REDACTED"
  }
}
KMSEntryobjectRequired
At least one of name, handler, content, whitelist or properties; same validation as creation.
KMSEntry.namestring
New display name.
KMSEntry.handlerstring
New alias, normalized to a slug; another entry using it returns 409.
KMSEntry.contentstring
New non-empty secret value. Omit to preserve the stored ciphertext.
KMSEntry.whitelistarray
Replacement list of exact IPv4/IPv6 addresses.
KMSEntry.propertiesobject
Replacement metadata object. Omit to preserve existing properties.

Responses

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.

GET
/2026-02-01/kms.json
Required permissionkms:read

Path, query, and header parameters

sizeinteger· query
Maximum records per page (max 100).
pageinteger· query
Target page index for pagination.

Responses

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.

GET
/2026-02-01/kms/{handler}.json
Required permissionkms:read

Path, query, and header parameters

handlerstring· pathRequired
The unique slug/identifier of the KMS entry.

Responses

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.

GET
/2026-02-01/kms/reveal/{handler}.json
Required permissionkms:read

Path, query, and header parameters

handlerstring· pathRequired
The KMS handler identifying the secret to reveal.

Responses

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.

On this page