To create a secret in Secrets Manager with AWS SDK v3, send CreateSecretCommand from @aws-sdk/client-secrets-manager with a Name and a SecretString (usually JSON.stringify of an object, maximum length 65,536). The first version gets the AWSCURRENT label. To change the value later, send PutSecretValueCommand: it adds a new version, moves AWSCURRENT to it and AWSPREVIOUS to the old one.
This guide is for Node.js and TypeScript developers who write secrets from code: a provisioning script that stores a generated database password, a service that saves a third-party token after OAuth, a CI job that seeds a new environment. Reading secrets is covered in the guide to getting a Secrets Manager secret value with SDK v3; this one covers the write side.
You’ll get the create call, a create-or-update function that survives retries and races, rollback with version labels, scheduled deletion, the cost and the IAM policy. Every sample was type-checked with strict tsc against @aws-sdk/client-secrets-manager 3.1141.0 and run in September 2026 against a mocked Secrets Manager client.
CreateSecret, PutSecretValue or UpdateSecret: which should you use?
| You want to… | Use | What happens |
|---|---|---|
| Store a new secret | CreateSecretCommand |
Creates the secret and, with a value, its first version labelled AWSCURRENT. Fails with ResourceExistsException if the name is taken. |
| Change the value | PutSecretValueCommand |
Adds a version, moves AWSCURRENT to it and AWSPREVIOUS to the old one. Can set custom labels with VersionStages. |
| Change description or KMS key | UpdateSecretCommand |
Updates metadata. With a new SecretString it also adds a version, like PutSecretValue. |
| Add or change tags | TagResourceCommand |
Tags aren’t part of UpdateSecret. |
| Change rotation | RotateSecretCommand |
The UpdateSecret API reference sends rotation changes here. |
Prefer PutSecretValue for value changes: it needs only secretsmanager:PutSecretValue, so a service can update its own token without being allowed to change the key or description.
Prerequisites
- Node.js 18 or later, a project with
"type": "module"(the samples use top-levelawait),@aws-sdk/client-secrets-managerandtsx. - Credentials the SDK can find; AWS SDK v3 credential providers explains profiles, SSO and roles.
- Optionally a customer managed KMS key. Without
KmsKeyId, Secrets Manager uses the AWS managed keyaws/secretsmanager, which can’t be used for secrets accessed from another account. Encrypting and decrypting with KMS in SDK v3 covers the key side.
How to create a secret in Secrets Manager with AWS SDK v3, step by step
- Pick a nameLetters, digits and
/_+=.@-, up to 512 characters. Paths likeprod/billing/dbmake IAM resource patterns easy. Don’t end it with a hyphen and six characters: Secrets Manager appends exactly that to the ARN. - Serialize the valueA JSON object in
SecretString. For database credentials you want rotated, match the JSON structure AWS documents for database secrets (username,password,hostand so on). - Choose the keySet
KmsKeyIdto a key ARN or alias for a customer managed key, or leave it out. - Tag itTags on create need
secretsmanager:TagResourceas well asCreateSecret. - Send CreateSecretCommandKeep the returned
ARN; it ends in six random characters, so a secret recreated with the same name gets a different ARN.
Example: create a secret with a JSON value
// create-secret.ts: create a Secrets Manager secret holding a JSON value, encrypted with your own KMS key.
// Usage: KMS_KEY_ID=alias/app-secrets npx tsx create-secret.ts
import { SecretsManagerClient, CreateSecretCommand } from "@aws-sdk/client-secrets-manager";
const client = new SecretsManagerClient({ region: process.env.AWS_REGION ?? "us-east-1" });
const value = {
username: "billing_app",
password: process.env.DB_PASSWORD ?? "change-me-before-use",
host: "billing.cluster-abc123.us-east-1.rds.amazonaws.com",
port: 5432,
};
const out = await client.send(new CreateSecretCommand({
Name: "prod/billing/db", // letters, digits and /_+=.@- only; don't end it with a hyphen and six characters
Description: "Billing service database login",
SecretString: JSON.stringify(value), // up to 65,536 bytes
KmsKeyId: process.env.KMS_KEY_ID, // omit to use the AWS managed key aws/secretsmanager
Tags: [
{ Key: "app", Value: "billing" },
{ Key: "env", Value: "prod" },
],
}));
console.log(`Created ${out.Name}`);
console.log(`ARN: ${out.ARN}`);
console.log(`Version ID: ${out.VersionId}`);
Created prod/billing/db
ARN: arn:aws:secretsmanager:us-east-1:123456789012:secret:prod/billing/db-Xy9Kq2
Version ID: 7c4e2a90-1b3d-4f6a-9e8c-2d5b7a1f0c34
The output comes from a run against a mocked client. The API reference warns that CloudTrail logs request parameters except SecretString and SecretBinary, so keep secrets out of the name, description and tags. Pass the password in from the environment or a generator, never from a command-line argument that lands in shell history.
Example: create or update a secret in one call
Provisioning code usually doesn’t know whether the secret exists yet. The usual pattern is to try PutSecretValue, fall back to CreateSecret on ResourceNotFoundException, and handle the race where another process creates it in between:
// secrets.ts: create-or-update, roll back and schedule deletion of Secrets Manager secrets with SDK v3.
import { randomUUID } from "node:crypto";
import {
SecretsManagerClient,
CreateSecretCommand,
DeleteSecretCommand,
ListSecretVersionIdsCommand,
PutSecretValueCommand,
ResourceExistsException,
ResourceNotFoundException,
UpdateSecretVersionStageCommand,
} from "@aws-sdk/client-secrets-manager";
export interface WriteResult {
action: "created" | "updated";
arn: string;
versionId: string;
}
/**
* Store a JSON value under `name`: a new version if the secret exists, otherwise a new secret.
* Pass the same `token` when you retry the same logical write, so a retry can't add a second version.
*/
export async function upsertSecret(
client: SecretsManagerClient,
name: string,
value: unknown,
opts: { kmsKeyId?: string; token?: string } = {},
): Promise<WriteResult> {
const SecretString = JSON.stringify(value);
const ClientRequestToken = opts.token ?? randomUUID();
try {
const out = await client.send(new PutSecretValueCommand({ SecretId: name, SecretString, ClientRequestToken }));
return { action: "updated", arn: out.ARN ?? "", versionId: out.VersionId ?? "" };
} catch (err) {
if (!(err instanceof ResourceNotFoundException)) throw err;
}
try {
const out = await client.send(new CreateSecretCommand({ Name: name, SecretString, KmsKeyId: opts.kmsKeyId, ClientRequestToken }));
return { action: "created", arn: out.ARN ?? "", versionId: out.VersionId ?? "" };
} catch (err) {
// Another process created the secret between our two calls: write the value as a new version instead.
if (!(err instanceof ResourceExistsException)) throw err;
const out = await client.send(new PutSecretValueCommand({ SecretId: name, SecretString, ClientRequestToken: randomUUID() }));
return { action: "updated", arn: out.ARN ?? "", versionId: out.VersionId ?? "" };
}
}
/** Version IDs that currently carry AWSCURRENT and AWSPREVIOUS. */
export async function currentAndPrevious(client: SecretsManagerClient, name: string): Promise<{ current?: string; previous?: string }> {
const out = await client.send(new ListSecretVersionIdsCommand({ SecretId: name }));
const find = (label: string) => out.Versions?.find((v) => v.VersionStages?.includes(label))?.VersionId;
return { current: find("AWSCURRENT"), previous: find("AWSPREVIOUS") };
}
/** Point AWSCURRENT back at the AWSPREVIOUS version. Secrets Manager moves AWSPREVIOUS to the old current. */
export async function rollbackSecret(client: SecretsManagerClient, name: string): Promise<string> {
const { current, previous } = await currentAndPrevious(client, name);
if (!current || !previous) throw new Error(`${name} has no AWSPREVIOUS version to roll back to`);
await client.send(new UpdateSecretVersionStageCommand({
SecretId: name,
VersionStage: "AWSCURRENT",
MoveToVersionId: previous,
RemoveFromVersionId: current,
}));
return previous;
}
/** Schedule deletion with a 7 to 30 day recovery window; RestoreSecret cancels it before the date. */
export async function scheduleDelete(client: SecretsManagerClient, name: string, days = 30): Promise<Date | undefined> {
if (!Number.isInteger(days) || days < 7 || days > 30) throw new Error("recovery window must be 7 to 30 days");
const out = await client.send(new DeleteSecretCommand({ SecretId: name, RecoveryWindowInDays: days }));
return out.DeletionDate;
}
// use-secrets.ts: write a secret twice, show the version labels, roll back, then schedule deletion.
// Usage: npx tsx use-secrets.ts
import { SecretsManagerClient } from "@aws-sdk/client-secrets-manager";
import { currentAndPrevious, rollbackSecret, scheduleDelete, upsertSecret } from "./secrets.js";
const client = new SecretsManagerClient({ region: process.env.AWS_REGION ?? "us-east-1" });
const name = "dev/webhooks/stripe";
const first = await upsertSecret(client, name, { signingSecret: "whsec_old" });
console.log(`${first.action} ${name} -> version ${first.versionId}`);
const second = await upsertSecret(client, name, { signingSecret: "whsec_new" });
console.log(`${second.action} ${name} -> version ${second.versionId}`);
console.log("labels:", await currentAndPrevious(client, name));
const restored = await rollbackSecret(client, name);
console.log(`rolled back: AWSCURRENT is now ${restored}`);
const deletionDate = await scheduleDelete(client, name, 7);
console.log(`scheduled for deletion on ${deletionDate?.toISOString()}`);
created dev/webhooks/stripe -> version b217bdf1-3e1f-4f4c-80c0-accd9ccb5058
updated dev/webhooks/stripe -> version 6d2f419b-90c9-4671-acf9-4b4ee82c8dda
labels: {
current: '6d2f419b-90c9-4671-acf9-4b4ee82c8dda',
previous: 'b217bdf1-3e1f-4f4c-80c0-accd9ccb5058'
}
rolled back: AWSCURRENT is now b217bdf1-3e1f-4f4c-80c0-accd9ccb5058
scheduled for deletion on 2027-07-29T09:00:00.000Z
The run used aws-sdk-client-mock to test the SDK v3 calls, with a fake that moves the labels the way the API reference describes. SDK v3 exports each exception as a class, so instanceof ResourceNotFoundException works; checking err.name works too.
How do versions, labels and ClientRequestToken work?
Every value change creates a version. Labels point at versions: AWSCURRENT is what GetSecretValue returns by default, and AWSPREVIOUS is the one before. Rotation adds AWSPENDING while it runs. Rolling back is just moving AWSCURRENT with UpdateSecretVersionStage; the API reference says Secrets Manager then moves AWSPREVIOUS to the version it came from, which rollbackSecret relies on.
ClientRequestToken becomes the new version’s VersionId and makes writes idempotent. In our test, SDK v3 filled it with a random UUID when we left it out, as the API reference says SDKs do. The rules for PutSecretValue and CreateSecret:
- A new token creates a new version.
- An existing token with the same value is ignored: the call succeeds and changes nothing.
- An existing token with a different value fails, because versions can’t be modified.
That’s why upsertSecret takes an optional token: generate one with Node’s crypto.randomUUID() per logical write and reuse it when you retry that write. Don’t derive the token from a hash of the value. If a value changes from A to B and back to A, the third write reuses A’s token, is silently ignored, and AWSCURRENT stays on B. UpdateSecret is stricter still: a token matching an existing version is always an error.
Warning: the API reference asks you not to call PutSecretValue or UpdateSecret at a sustained rate of more than once every 10 minutes. Secrets Manager keeps the 100 most recent versions plus all versions from the last 24 hours, so faster writes pile up versions until you hit the quota. Values that change every few seconds belong in DynamoDB or a cache, not in a secret.
How do you delete and restore a secret?
DeleteSecretCommand schedules deletion after a recovery window of 7 to 30 days (30 if you pass neither option). Until then, the secret’s value can’t be read and RestoreSecretCommand cancels the deletion. ForceDeleteWithoutRecovery: true deletes it with no window and no way back. The deletion runs in the background, so creating a secret with the same name immediately afterwards needs back-off and retry. A primary secret with replicas can’t be deleted until the replicas are removed. To decide what to delete, the script to find unused Secrets Manager secrets checks when each was last read.
What does a Secrets Manager secret cost?
As of September 2026, the AWS Price List for AWS Secrets Manager (published 11 September 2026) shows these rates in US East (N. Virginia):
| Item | Price |
|---|---|
| Secret storage | $0.40 per secret per month |
API requests (CreateSecret, PutSecretValue, GetSecretValue and others) |
$0.05 per 10,000 |
Worked example: 3 environments × 12 services × 2 secrets each is 72 secrets, or 72 × $0.40 = $28.80 a month. Writing each one weekly adds about 312 requests a month, well under a cent. Reads dominate the request line; caching them, as the read guide shows, keeps it small. Customer managed KMS keys add their own monthly and request charges.
Which IAM permissions do secret writes need?
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "WriteAppSecrets",
"Effect": "Allow",
"Action": [
"secretsmanager:CreateSecret",
"secretsmanager:PutSecretValue",
"secretsmanager:TagResource",
"secretsmanager:ListSecretVersionIds",
"secretsmanager:UpdateSecretVersionStage",
"secretsmanager:DeleteSecret",
"secretsmanager:RestoreSecret"
],
"Resource": [
"arn:aws:secretsmanager:us-east-1:123456789012:secret:prod/billing/*",
"arn:aws:secretsmanager:us-east-1:123456789012:secret:dev/webhooks/*"
]
},
{
"Sid": "UseSecretsKey",
"Effect": "Allow",
"Action": ["kms:GenerateDataKey", "kms:Decrypt"],
"Resource": "arn:aws:kms:us-east-1:123456789012:key/1234abcd-12ab-34cd-56ef-1234567890ab"
}
]
}
The API reference lists kms:GenerateDataKey and kms:Decrypt for creating a secret with a customer managed key; UpdateSecret with a new key also needs kms:Encrypt. Trim the first statement to what each caller does. The IAM policy generator for TypeScript AWS SDK code lists the actions your code uses, and reviewing an IAM policy for least privilege helps tighten the result.
Troubleshooting and common mistakes
| Error | Usual cause |
|---|---|
ResourceExistsException |
The name is already taken when you call CreateSecret. |
ResourceNotFoundException |
Wrong name or Region on PutSecretValue; the upsert pattern turns it into a create. |
InvalidRequestException |
The secret is scheduled for deletion (restore it first), or it’s managed by another service such as RDS and must be changed there. |
EncryptionFailure |
The KMS key isn’t available: disabled, pending deletion or otherwise in an invalid state. |
LimitExceededException |
A quota, often too many versions from frequent writes. |
AccessDeniedException |
Missing Secrets Manager or KMS permission; see troubleshooting AWS IAM access denied errors. |
- Secrets as environment variables. Writing a secret and then copying it into a Lambda or ECS environment undoes the point; the scripts to find secrets in Lambda environment variables and find secrets in ECS task definitions catch it.
- Hand-rolled rotation. For database credentials, use managed or Lambda rotation instead of a cron job calling
PutSecretValue; the script to find secrets without rotation shows which secrets need it. - Plain configuration. Non-secret settings are cheaper as parameters; see getting an SSM parameter with SDK v3.
- Coming from SDK v2.
secretsManager.createSecret(params).promise()becomesclient.send(new CreateSecretCommand(params)). The free AWS SDK v2 to v3 converter handles the rest of a file.
Frequently asked questions
How do I update a secret value with AWS SDK v3?
Send PutSecretValueCommand with SecretId and the new SecretString. It creates a new version and moves AWSCURRENT to it; the old value keeps AWSPREVIOUS.
Do I need to set ClientRequestToken?
No. SDK v3 generates a UUID when you leave it out. Set it yourself only when you retry the same write and want the retry to be ignored instead of creating a second version.
How do I create a secret if it doesn’t exist and update it if it does?
Call PutSecretValue, catch ResourceNotFoundException and call CreateSecret; if that throws ResourceExistsException, another process won the race, so write with PutSecretValue again.
Can I reuse a secret name right after deleting it?
Not while it’s scheduled for deletion. Restore it, or delete it with ForceDeleteWithoutRecovery and retry the create with back-off, since the deletion finishes in the background.
How big can a secret be?
The API reference gives SecretString and SecretBinary a maximum length of 65,536 each.
Related guides
Ask your AWS account in plain English
Your first 15 runs are free, with no OpenAI key needed.
npx chatwithcloud
