Photo by Silas Köhler on Unsplash
To call Cognito AdminCreateUser with AWS SDK v3, send an AdminCreateUserCommand from @aws-sdk/client-cognito-identity-provider with the UserPoolId, a Username and UserAttributes such as email. Cognito emails a temporary password and the user starts in FORCE_CHANGE_PASSWORD. Add MessageAction: "SUPPRESS" to send nothing, then AdminSetUserPassword with Permanent: true.
You create Cognito users from code when self sign-up is off: an admin panel that invites staff, a migration from another identity store, a B2B app where each customer’s admin adds their team, or test users for a staging pool. The admin API does all of that, but the defaults surprise people: an invitation goes out unless you say otherwise, a re-run fails on the first existing user, and the user can’t sign in until they change a password they may never have received.
This guide is for Node.js and TypeScript developers. You’ll get a module that wraps Cognito AdminCreateUser with AWS SDK v3 for both the invite and the silent flow, sets permanent passwords, adds users to groups and can be re-run safely, plus the IAM policy and the errors you’ll meet.
What does AdminCreateUser do?
It creates a user in a user pool as an administrator, authorized with IAM credentials rather than a user’s token. What happens next depends on two parameters:
| Flow | Parameters | Result |
|---|---|---|
| Invite | No MessageAction, DesiredDeliveryMediums: ["EMAIL"] |
Cognito generates a temporary password and emails it using the pool’s invitation template. Status FORCE_CHANGE_PASSWORD until the user signs in and sets a new one |
| Silent | MessageAction: "SUPPRESS", plus AdminSetUserPassword with Permanent: true |
No message. The user can sign in immediately with the password you set; status CONFIRMED |
| Resend | MessageAction: "RESEND" for an existing user |
Sends a new invitation with a new temporary password and resets its expiry |
| Passwordless | No TemporaryPassword, pool with email or SMS one-time codes enabled |
A user with no password who signs in with codes |
A temporary password is valid for the pool’s TemporaryPasswordValidityDays, 7 days by default. After that, the user can’t sign in until you resend the invitation or set a password. Note the default for DesiredDeliveryMediums is SMS, so set EMAIL explicitly for email invites.
Prerequisites
- Node.js 18 or later, TypeScript and
tsx, with"type": "module"inpackage.json. - The
@aws-sdk/client-cognito-identity-providerpackage. - A user pool ID (such as
eu-west-1_AbCdEf123) and any groups you’ll assign, created beforehand withCreateGroupor the console. Keep the pool ID in configuration, for example with the guide to get SSM Parameter Store values with AWS SDK v3. - IAM credentials the SDK can resolve, set up as in AWS SDK v3 credential providers: fromIni, fromSSO and assume role. Admin operations never accept a user’s access token.
How to call Cognito AdminCreateUser with AWS SDK v3, step by step
- Create the client in the pool’s RegionThe Region is the prefix of the pool ID. A client in the wrong Region returns
ResourceNotFoundException. - Check how users sign inIf the pool signs users in with email only, pass the email as
Username; Cognito stores a generated username instead, returned in the response. Later admin calls accept the email as an alias. - Set attributes
emailis required for email invites. Setemail_verifiedto"true"only if you’ve verified the address yourself; otherwise the user can’t use it to reset a forgotten password until it’s verified. Custom attributes need thecustom:prefix. - Choose invite or silentOmit
MessageActionto send the invitation, or setSUPPRESS. - Set a permanent password in the silent flow
AdminSetUserPasswordwithPermanent: truemoves the user toCONFIRMED. - Add groups
AdminAddUserToGroupper group. Group membership appears in thecognito:groupsclaim of the user’s tokens. - Make it re-runnableCatch
UsernameExistsExceptionand read the user withAdminGetUserinstead of failing.
Example: a re-runnable user creation module
// cognito-users.ts
// Create Cognito user pool users from code with AWS SDK for JavaScript v3:
// AdminCreateUser (invite or silent), AdminSetUserPassword (permanent), AdminAddUserToGroup,
// and an idempotent wrapper that treats UsernameExistsException as "already there".
import { randomBytes } from "node:crypto";
import {
AdminAddUserToGroupCommand,
AdminCreateUserCommand,
AdminGetUserCommand,
AdminSetUserPasswordCommand,
CognitoIdentityProviderClient,
UsernameExistsException,
type AttributeType,
} from "@aws-sdk/client-cognito-identity-provider";
const cognito = new CognitoIdentityProviderClient({}); // Region must match the user pool's Region
export interface NewUser {
email: string;
name?: string;
groups?: string[];
/** "invite": Cognito emails a temporary password. "silent": no message; you set the password. */
mode: "invite" | "silent";
password?: string; // required for "silent"
}
export interface CreatedUser {
username: string; // the value Cognito stored; a generated ID in email-only sign-in pools
sub: string;
status: string;
created: boolean;
}
const attr = (Name: string, Value: string): AttributeType => ({ Name, Value });
/** A temporary password: 20 random characters plus one of each class most pool policies require. */
export function temporaryPassword(): string {
return `${randomBytes(15).toString("base64url")}Aa1!`;
}
export async function createUser(userPoolId: string, u: NewUser): Promise<CreatedUser> {
const attributes = [attr("email", u.email), attr("email_verified", "true")];
if (u.name) attributes.push(attr("name", u.name));
let created = true;
let username = u.email;
let sub = "";
let status = "";
try {
const { User } = await cognito.send(
new AdminCreateUserCommand({
UserPoolId: userPoolId,
Username: u.email,
UserAttributes: attributes,
DesiredDeliveryMediums: ["EMAIL"],
// Omit MessageAction to send the invitation; SUPPRESS sends nothing.
MessageAction: u.mode === "silent" ? "SUPPRESS" : undefined,
TemporaryPassword: u.mode === "silent" ? temporaryPassword() : undefined,
}),
);
username = User?.Username ?? u.email;
sub = User?.Attributes?.find((a) => a.Name === "sub")?.Value ?? "";
status = User?.UserStatus ?? "";
} catch (err) {
if (!(err instanceof UsernameExistsException)) throw err;
// Already created by an earlier run: read it instead of failing, so the script can be re-run.
created = false;
const existing = await cognito.send(new AdminGetUserCommand({ UserPoolId: userPoolId, Username: u.email }));
username = existing.Username ?? u.email;
sub = existing.UserAttributes?.find((a) => a.Name === "sub")?.Value ?? "";
status = existing.UserStatus ?? "";
}
// Silent mode: replace the throwaway temporary password with a permanent one, so status becomes CONFIRMED.
if (u.mode === "silent" && created) {
if (!u.password) throw new Error(`silent mode needs a password for ${u.email}`);
await cognito.send(
new AdminSetUserPasswordCommand({ UserPoolId: userPoolId, Username: username, Password: u.password, Permanent: true }),
);
status = "CONFIRMED";
}
for (const group of u.groups ?? []) {
// The group must already exist (CreateGroup); a missing group throws ResourceNotFoundException.
await cognito.send(new AdminAddUserToGroupCommand({ UserPoolId: userPoolId, Username: username, GroupName: group }));
}
return { username, sub, status, created };
}
A small import script that reads users from a JSON file:
// create-users.ts
// Usage: AWS_PROFILE=admin AWS_REGION=eu-west-1 npx tsx create-users.ts eu-west-1_AbCdEf123 users.json
// users.json: [{ "email": "[email protected]", "name": "Ana", "groups": ["editors"], "mode": "invite" }]
import { readFileSync } from "node:fs";
import { setTimeout as sleep } from "node:timers/promises";
import { createUser, type NewUser } from "./cognito-users.js";
const [userPoolId, file] = process.argv.slice(2);
if (!userPoolId || !file) throw new Error("usage: create-users.ts <userPoolId> <users.json>");
const users = JSON.parse(readFileSync(file, "utf8")) as NewUser[];
let failures = 0;
for (const u of users) {
try {
const r = await createUser(userPoolId, u);
console.log(`${r.created ? "created" : "exists "} ${u.email} -> ${r.username} ${r.status} groups=${(u.groups ?? []).join(",") || "-"}`);
} catch (err) {
failures++;
console.error(`failed ${u.email}: ${err instanceof Error ? `${err.name}: ${err.message}` : String(err)}`);
}
await sleep(100); // stay well under the user-creation request rate for large imports
}
if (failures) process.exit(1);
[
{ "email": "[email protected]", "name": "Ana", "groups": ["editors"], "mode": "invite" },
{ "email": "[email protected]", "name": "CI bot", "groups": ["readers"], "mode": "silent", "password": "load-this-from-secrets-manager" }
]
npm install @aws-sdk/client-cognito-identity-provider
npm install --save-dev tsx typescript @types/node
npm pkg set type=module
AWS_PROFILE=admin AWS_REGION=eu-west-1 npx tsx create-users.ts eu-west-1_AbCdEf123 users.json
# created [email protected] -> 3264f4a8-7011-70c6-9e1a-5a8f2c0d4b11 FORCE_CHANGE_PASSWORD groups=editors
# created [email protected] -> 7274b4c8-e0a1-7055-2b3c-9d8e7f6a5b40 CONFIRMED groups=readers
# Run it again: nothing is duplicated
# exists [email protected] -> 3264f4a8-7011-70c6-9e1a-5a8f2c0d4b11 FORCE_CHANGE_PASSWORD groups=editors
# exists [email protected] -> 7274b4c8-e0a1-7055-2b3c-9d8e7f6a5b40 CONFIRMED groups=readers
IDs are illustrative; this pool signs users in with email, so the stored usernames are generated. Don’t keep real passwords in a JSON file: load them at runtime with the guide to get a Secrets Manager secret value with SDK v3, and never put them in function configuration, which the script to find secrets in Lambda environment variables catches.
Invite or silent: which flow should you use?
Use the invite flow for people. The user proves they control the mailbox by using the temporary password, and you never handle their real password. Customize the invitation template in the pool, and keep the {####} placeholder: without it, Cognito doesn’t deliver the invitation at all. For production volumes, configure the pool to send through Amazon SES rather than the default Cognito sender, which has a small daily quota; the guide to send email with Amazon SES and SDK v3 covers verified identities and the sandbox.
Use the silent flow for service accounts, load tests and migrations where you already hold a password that meets the pool’s policy. Treat those passwords like any other credential: long, random, stored in a secrets manager and rotated. The OWASP Authentication Cheat Sheet recommends favoring length over composition rules and allowing passwords of at least 64 characters, which is a good baseline when you set the pool’s password policy.
After creation, store the user’s sub as the key in your own database, not the email: the sub never changes, while emails do. The guide to DynamoDB UpdateItem with AWS SDK v3 shows a conditional upsert that suits a profile table keyed by sub.
Which IAM permissions does creating users need?
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "CreateUsersInOnePool",
"Effect": "Allow",
"Action": [
"cognito-idp:AdminCreateUser",
"cognito-idp:AdminGetUser",
"cognito-idp:AdminSetUserPassword",
"cognito-idp:AdminAddUserToGroup"
],
"Resource": "arn:aws:cognito-idp:eu-west-1:123456789012:userpool/eu-west-1_AbCdEf123"
}
]
}
Drop AdminSetUserPassword if you only use the invite flow; it lets the caller take over any account in the pool. AdminAddUserToGroup is also sensitive when groups map to IAM roles or admin rights in your app. The guide to review a generated IAM policy for least privilege explains how to split these across roles, and the free IAM policy generator for TypeScript derives the list from cognito-users.ts. To double-check actions by hand, see how to find the IAM actions your AWS SDK for JavaScript code needs.
Troubleshooting AdminCreateUser errors
UsernameExistsException. The user already exists. The module reads it instead; to send a fresh invitation, call again withMessageAction: "RESEND".AliasExistsException. You setemail_verifiedtotrueand another user already has that verified email. Fix the duplicate, or setForceAliasCreationto move the alias, which locks the other user out of signing in with it.InvalidPasswordException. The temporary or permanent password doesn’t meet the pool’s policy. Read the policy withDescribeUserPool.InvalidParameterException. A required attribute is missing, a custom attribute lackscustom:, or the username format doesn’t match the pool’s sign-in settings.CodeDeliveryFailureException, or no email arrives. Check the pool’s email configuration and SES sending status, and that the invitation template contains{####}.TooManyRequestsException. User creation has its own request-rate quota. The SDK retries throttling errors, and the guide to configure retries and timeouts in AWS SDK v3 shows how to allow more attempts; for large imports, slow down or request an increase.AccessDeniedExceptionorNotAuthorizedException. Missingcognito-idppermissions, or a policy scoped to another pool ARN. The steps to troubleshoot AWS IAM access denied errors decode the first.
Limits and pricing
The module creates users one at a time. For hundreds of thousands of users, a CSV user import job (CreateUserImportJob) is faster, and imported users start in RESET_REQUIRED; for users you can authenticate against an old system, a user migration Lambda trigger moves them on first sign-in. AdminCreateUser can also run your pool’s Lambda triggers, which receive ClientMetadata. Cognito bills user pools by monthly active users, with rates that depend on the pool’s feature plan (Lite, Essentials or Plus); check the Amazon Cognito pricing page before bulk-creating test users in a paid plan.
Moving code from SDK v2? The AWS SDK v2 to v3 code converter drafts adminCreateUser calls as commands, and the guide to migrate a Node.js app from AWS SDK v2 to v3 covers error handling, which changes most. To look up a user pool’s settings without writing code, ChatWithCloud answers questions about your AWS account in plain English from the terminal; how ChatWithCloud works explains what runs locally.
Frequently asked questions
How do I create a Cognito user without sending an email?
Call AdminCreateUser with MessageAction: "SUPPRESS". The user gets no message; set their password with AdminSetUserPassword, using Permanent: true if they should sign in without changing it.
How do I get a Cognito user out of FORCE_CHANGE_PASSWORD?
Either the user signs in with the temporary password and answers the NEW_PASSWORD_REQUIRED challenge, or an administrator calls AdminSetUserPassword with Permanent: true, which sets the status to CONFIRMED.
How long is a Cognito temporary password valid?
For the pool’s TemporaryPasswordValidityDays, which defaults to 7 days. After it expires, call AdminCreateUser again with MessageAction: "RESEND", or set a new password.
Can I call AdminCreateUser from the browser?
No. Admin operations need IAM credentials, so call them from a backend, a Lambda function or a script. Browsers use SignUp with an app client instead.
Related guides
Ask your AWS account in plain English
Your first 15 runs are free, with no OpenAI key needed.
npx chatwithcloud