Error Codes & Troubleshooting
Complete reference for all CLI error codes and SDK error classes, with solutions for each.
Complete reference for all CLI error codes and SDK error classes, with solutions for each.
--jsonMost codes reach --json exactly as the tables below list them. Two families are emitted with a prefix added.
PULUMI_*: a failure during infrastructure deployment is emitted as PULUMI_ plus the code in the tables, so STACK_LOCKED arrives as PULUMI_STACK_LOCKED. A Pulumi failure the CLI could not classify is emitted as PULUMI_PULUMI_ERROR.AWS_*: an error raised by the AWS SDK itself is emitted as AWS_ plus the SDK's own error name, for example AWS_ExpiredTokenException or AWS_AccessDenied. This family is open-ended and not listed here. The two AWS_* rows in the tables (AWS_THROTTLED, AWS_LIMIT_EXCEEDED) are Wraps' own codes, not members of it.Match on the documented code. Treat an unrecognised AWS_* or PULUMI_* code as the underlying tool's error, surfaced verbatim.
| Code | Message | Solution |
|---|---|---|
NO_AWS_CREDENTIALS | AWS credentials not found | Set up AWS SSO (aws configure sso), IAM access keys (aws configure), environment variables, or AWS_PROFILE |
PROFILE_NOT_FOUND | AWS profile "<profile>" not found | List available profiles, or configure a new one: aws configure --profile <profile> |
CREDENTIALS_FILE_MISSING | AWS credentials file not found | Set up AWS SSO, IAM access keys, environment variables, or AWS_PROFILE |
ACCESS_KEY_INVALID | AWS access key is invalid or has been deactivated | Check the IAM console, run aws configure, or generate new access keys |
SESSION_TOKEN_EXPIRED | AWS session token has expired | SSO users: aws sso login. Assumed roles: re-run your assume-role command |
SSO_SESSION_EXPIRED | AWS SSO session has expired for profile "<profile>" | Run aws sso login --profile <profile> |
NOT_AUTHENTICATED | Not authenticated to Wraps Platform | Run wraps auth login, or provide --token / WRAPS_API_KEY |
ORG_NOT_FOUND | Could not determine organization | Pass --org, or sign in first: wraps auth login |
| Code | Message | Solution |
|---|---|---|
IAM_PERMISSION_DENIED | Permission denied: <action> on <resource> | Your credentials lack the permission. View required permissions: wraps permissions --json |
SES_PERMISSION_DENIED | SES permission denied: <action> | wraps permissions --service email --json |
DYNAMODB_PERMISSION_DENIED | DynamoDB permission denied | Needs CreateTable, DeleteTable, DescribeTable, UpdateTable |
LAMBDA_PERMISSION_DENIED | Lambda permission denied | Needs CreateFunction, UpdateFunctionCode, DeleteFunction |
EVENTBRIDGE_PERMISSION_DENIED | EventBridge permission denied | Needs PutRule, PutTargets, DeleteRule |
SQS_PERMISSION_DENIED | SQS permission denied | Needs CreateQueue, DeleteQueue, GetQueueAttributes |
CLOUDWATCH_LOGS_PERMISSION_DENIED | CloudWatch Logs permission denied | Needs logs:DescribeLogGroups, logs:FilterLogEvents, logs:StartLiveTail |
ROUTE53_PERMISSION_DENIED | Route53 permission denied | Needs ChangeResourceRecordSets, ListHostedZones (optional — add DNS records manually instead) |
IAM_ENTITY_NOT_FOUND | IAM entity not found | Run wraps platform connect |
| Code | Message | Solution |
|---|---|---|
INVALID_REGION | Invalid AWS region: <region> | Use a valid AWS region like us-east-1, eu-west-1, ap-southeast-1 |
REGION_REQUIRED | Region is required and could not be determined | Pass --region or set AWS_REGION |
REGION_REQUIRED_FOR_SET | Could not determine which Region to change | Pass --region <r> with the Region you want to change |
AWS_THROTTLED | AWS request was throttled | Wait and retry. Request a quota increase if this happens repeatedly |
AWS_LIMIT_EXCEEDED | AWS service limit exceeded | Request a quota increase in Service Quotas |
| Code | Message | Solution |
|---|---|---|
NO_STACK | No Wraps infrastructure found in this AWS account | Run wraps email init |
STACK_EXISTS | Stack "<stackName>" already exists | To update: wraps email upgrade. To remove: wraps destroy --stack <stackName> |
STACK_LOCKED | The Pulumi stack is locked from a previous run | Remove ~/.wraps/pulumi/.pulumi/locks (local), or delete the lock object under .pulumi/locks/ in your wraps-state-* bucket (S3) |
PENDING_OPERATIONS | The Pulumi stack has unresolved operations from a previous deployment | Run pulumi refresh to reconcile state, or pulumi stack export to inspect it. If a deployment is genuinely still running elsewhere, wait for it instead. |
PULUMI_ERROR | Infrastructure deployment failed: <message> | The message above is Pulumi's own output. Re-run the command if it looks transient; see the troubleshooting guide if it persists. |
PULUMI_NOT_INSTALLED | Pulumi CLI is not installed | Install via brew/curl/choco, or download from pulumi.com/docs/install |
RESOURCE_CONFLICT | Resource already exists: <resourceName> | Diagnose first: wraps email doctor. Only run --cleanup if it lists orphaned resources. |
S3_STATE_BUCKET_CREATION_FAILED | Failed to create S3 state bucket: <bucketName> | Ensure s3:CreateBucket, s3:PutBucketEncryption, s3:PutBucketVersioning, or export WRAPS_LOCAL_ONLY=1 |
S3_STATE_ACCESS_DENIED | Access denied to S3 state bucket | Ensure s3:GetObject, s3:PutObject, s3:ListBucket on wraps-state-*, or export WRAPS_LOCAL_ONLY=1 |
STATE_MIGRATION_FAILED | Failed to migrate Pulumi state to S3 | Local state is still intact. export WRAPS_LOCAL_ONLY=1 to skip migration |
SELFHOST_NO_LOG_GROUPS | No self-hosted log groups found in <region> | Check wraps selfhost status, or target a different region |
SELFHOST_NO_LOG_GROUPS_FOR_SOURCE | No <source> log groups in this deployment | Drop the filter: wraps selfhost logs |
LAMBDA_FUNCTION_NOT_FOUND | Agent enforcer Lambda not found | Deploy it: wraps email agent create |
DYNAMODB_TABLE_NOT_FOUND | Agent policy table not found | Deploy it: wraps email agent create |
| Code | Message | Solution |
|---|---|---|
SES_MESSAGE_REJECTED | SES rejected the message: <detail> | Sandbox with unverified recipient, unverified sender identity, or receiving-only domain. Check wraps email status / wraps email doctor |
SES_MAIL_FROM_NOT_VERIFIED | SES MAIL FROM domain is not verified: <detail> | Check DNS records: wraps email verify |
SES_ACCOUNT_SENDING_PAUSED | SES account-level sending is paused | Check the SES console Reputation Dashboard |
SES_CONFIG_SET_SENDING_PAUSED | SES configuration set sending is paused | Resume it in the SES console, or send without that configuration set |
SES_CONFIG_SET_MISSING | SES configuration set does not exist: <detail> | Create it in the SES console, switch regions, or remove ConfigurationSetName |
EVENT_DESTINATION_NOT_FOUND | Event destination not found for <domain> | Run wraps email upgrade |
INVALID_EVENT_DESTINATION | Event destination for <domain> is not an EventBridge destination | Run wraps email upgrade |
EVENT_TYPES_MISSING_SUPPRESSION_EVENTS | eventTracking.events is missing required event type(s): <missing> | BOUNCE and COMPLAINT must always be included in eventTracking.events |
INVALID_SES_PRICING_PLAN | No pricing plan specified | Pass --set with one of the valid pricing plans |
SES_PRICING_PLAN_CHANGE_REJECTED | SES rejected the pricing plan change: <detail> | Check the current plan: wraps email plan |
SES_VDM_CHANGE_REJECTED | SES rejected the VDM change: <detail> | Check the current state: wraps email vdm |
INVALID_VALIDATION_THRESHOLD | Invalid validation threshold: <detail> | Valid values: medium, high, managed |
SUPPRESSION_OPTIONS_MISSING | Configuration set <name> has no suppression options, so Auto Validation cannot be added without changing its suppression behaviour | Set suppression explicitly first: wraps email domains config --suppress-bounce --suppress-complaint |
CONFLICTING_VDM_FLAGS | Pass either --enable or --disable, not both | Use one of the two flags |
SENDING_POOL_CONFLICT | The configuration set already sends through dedicated IP pool "<pool>" | Wraps will not replace a pool it did not attach. Detach it in the SES console or leave managed dedicated IPs off. |
MANAGED_POOL_NAME_TAKEN | A STANDARD dedicated IP pool named "wraps-email-managed" already exists | Wraps will not convert it (MANAGED is irreversible). Rename or delete that pool, then retry. |
MANAGED_DEDICATED_IPS_UNAVAILABLE | Managed dedicated IPs are unavailable on your SES plan: <reason> | Check your current SES pricing plan: wraps email plan |
PRODUCTION_ACCESS_PENDING | A production access request is already under review for this account | Wait for AWS to finish the current review before submitting another. Run wraps email production-access to see its status. |
PRODUCTION_ACCESS_REJECTED | AWS rejected the request: <detail> | Check the website URL (must be a full https:// URL) and contact addresses, then retry. |
PRODUCTION_ACCESS_ALREADY_ENABLED | Production access is already enabled for this account in <region> | Nothing to do — run wraps email production-access to confirm |
INVALID_WEBSITE_URL | Invalid --website: <value> | Pass a full URL starting with http:// or https://, up to 1000 characters |
INVALID_MAIL_TYPE | Invalid --mail-type: <value> | Valid values: transactional, marketing |
INVALID_CONTACT_EMAIL | Invalid contact email: <value> | Pass valid email addresses, comma-separated, via --contact (max 4) |
| Code | Message | Solution |
|---|---|---|
INVALID_TRACKING_DOMAIN | Invalid --tracking-domain: <problem> | A tracking domain must be a hostname under the sending domain, e.g. track.news.example.com for news.example.com |
PRIMARY_TRACKING_DOMAIN_MANAGED_BY_PULUMI | <domain> is the primary domain — its tracking domain is managed by wraps email upgrade | Run wraps email upgrade and choose "tracking". domains config sets tracking domains on additional domains only. |
TRACKING_DOMAIN_REQUIRED | <domain> has no tracking domain yet | Set one before enabling HTTPS: wraps email domains config --domain <domain> --tracking-domain <host> |
PRIMARY_TRACKING_HTTPS_MANAGED_BY_PULUMI | <domain> is the primary domain — its HTTPS tracking is managed by wraps email upgrade | Run wraps email upgrade and choose "tracking" |
TRACKING_HTTPS_PROVISION_FAILED | Failed to provision HTTPS for tracking domain: <detail> | Usually the ACM certificate is not ISSUED yet (validation takes 5-30 minutes), the tracking domain is already claimed by another CloudFront distribution, or the account lacks acm:RequestCertificate / cloudfront:CreateDistributionWithTags. Retry with wraps email domains config --domain <domain> --tracking-https. |
| Code | Message | Solution |
|---|---|---|
INBOUND_REGION_NOT_SUPPORTED | SES email receiving is not supported in <region> | Deploy in us-east-1, us-west-2, or eu-west-1 |
INBOUND_REQUIRES_OUTBOUND | Inbound email requires outbound email infrastructure | Run wraps email init, then wraps email inbound init |
RECEIPT_RULE_SET_CONFLICT | Another receipt rule set is active: <activeRuleSet> | Wraps will activate wraps-inbound-rules, deactivating the current set |
INBOUND_MX_CONFLICT | <domain> already has mail routed to it | Pick a subdomain with no existing MX record, e.g. wraps email inbound add --subdomain support. Or run interactively (without --yes) to confirm and proceed anyway. |
INBOUND_TEST_SEND_FAILED | Failed to send inbound test email to <recipient> | Check wraps email status / wraps email doctor |
INBOUND_TEST_MAIL_FROM_NOT_VERIFIED | Custom MAIL FROM domain is not verified for <domain> | Verify DNS records: wraps email verify |
INBOUND_TEST_MESSAGE_REJECTED | SES rejected the inbound test send: <message> | Sandbox with unverified recipient, unverified sender domain, or receiving-only domain. Check wraps email status / doctor |
INBOUND_TEST_SENDING_PAUSED | SES sending is paused for this account | Check the SES console Reputation Dashboard |
INBOUND_TEST_PERMISSION_DENIED | IAM permission denied: ses:SendEmail in <region> | wraps permissions --service email --json |
| Code | Message | Solution |
|---|---|---|
REPLY_SECRET_PARAMETER_MISSING | SSM parameter for <domain> was not created | Run wraps email reply status to diagnose, or retry the init |
REPLY_REQUIRES_INBOUND | Reply threading requires inbound email infrastructure | Deploy inbound first: wraps email inbound init |
REPLY_NO_INBOUND_DOMAINS | No inbound domains configured | Add one: wraps email inbound add --domain yourapp.com |
REPLY_INBOUND_DOMAIN_NOT_FOUND | Domain <target> is not configured for inbound email | Add it to inbound first: wraps email inbound add <target> |
REPLY_ALREADY_ENABLED | Reply threading is already enabled for <target> | To rotate the signing secret: wraps email reply rotate --domain <target> |
REPLY_MISSING_DOMAIN | Specify a domain or use --all | wraps email reply init --domain yourapp.com, or --all |
REPLY_ROTATE_MISSING_DOMAIN | --domain is required for rotate | wraps email reply rotate --domain yourapp.com |
REPLY_NOT_ENABLED | Reply threading is not enabled | Enable it first: wraps email reply init --domain yourapp.com |
REPLY_DOMAIN_NOT_ENABLED | Reply threading is not enabled for <domain> | Enable it first: wraps email reply init --domain <domain> |
REPLY_DESTROY_MISSING_DOMAIN | Specify a domain or use --all | wraps email reply destroy --domain yourapp.com, or --all |
REPLY_DECODE_MISSING_ADDRESS | Usage: wraps email reply decode <token>@r.mail.yourapp.com | Provide a signed reply address |
REPLY_DECODE_MALFORMED_ADDRESS | Address must be in the form <token>@r.mail.example.com | Pass a full signed reply address |
| Code | Message | Solution |
|---|---|---|
SMS_NOT_CONFIGURED | SMS infrastructure not found | Run wraps sms init |
SMS_PHONE_NOT_VERIFIED | Phone number registration not complete | Toll-free numbers require registration (15+ days). Check status in the AWS console |
SMS_OPTED_OUT | Destination number <phoneNumber> has opted out | The recipient can opt back in by texting START |
SMS_SPENDING_LIMIT | AWS SMS spending limit reached | Request a spending limit increase in the AWS console |
SMS_INVALID_PHONE_NUMBER | Invalid phone number format: <phoneNumber> | Use E.164 format, e.g. +14155551234 |
SMS_INVALID_COUNTRIES | Invalid --countries value: <raw> | Comma-separated ISO 3166-1 alpha-2 codes, e.g. --countries US,CA,GB |
SMS_INVALID_VOLUME | Invalid --volume value: <raw> | Positive whole number of messages per month |
SMS_SIMULATOR_LIMIT | Simulator daily message limit reached (100 messages) | Upgrade: wraps sms upgrade --phone-type toll-free |
| Code | Message | Solution |
|---|---|---|
SMTP_CREDENTIALS_NOT_FOUND | SMTP credentials not found | wraps email upgrade and select "Enable SMTP credentials" |
SMTP_REQUIRES_SENDING | SMTP credentials require email sending to be enabled | wraps email upgrade and select "Custom configuration" |
| Code | Message | Solution |
|---|---|---|
WRAPS_CONFIG_NOT_FOUND | wraps/wraps.config.ts not found | Initialize templates first: wraps email templates init |
TEMPLATE_COMPILATION_FAILED | Failed to compile template "<name>": <error> | Check your template for syntax errors and valid imports |
TEMPLATE_PUSH_FAILED | Failed to push template "<name>": <error> | Check your API key and network connection |
TEMPLATES_DIR_EXISTS | wraps/ directory already exists | Use --force to overwrite: wraps email templates init --force |
| Code | Message | Solution |
|---|---|---|
NON_INTERACTIVE_INPUT | <what> is required in non-interactive mode | Pass the required flag, or run in an interactive terminal |
UNKNOWN_COMMAND | Unknown <command kind>: <what was typed> | Run wraps --help, or use one of the commands listed in the error's suggestion |
MISSING_REQUIRED_FLAG | A required flag is missing in JSON mode | Provide the flag named in the error message |
JSON_REQUIRES_FORCE | --force flag is required in JSON mode for destructive operations | Add --force to the command |
CONFIRMATION_REQUIRED | Confirmation required to change your SES pricing plan | Pass --yes to skip the confirmation prompt |
OPERATION_CANCELLED | Operation cancelled | Pass --region to skip the interactive prompt |
INVALID_LOG_SOURCE | Unknown --source value: <value> | Valid sources: api, web, workers, other, all |
INVALID_LOG_WINDOW | Invalid --since value: <value> | Use a positive number followed by s, m, h, or d |
UNKNOWN_ERROR | An unexpected error occurred | The thrown value was not a recognized WrapsError. Check the CLI logs for details |
Every command exits 0 on success and 1 on error, with one exception.
| Command | Exit code | Meaning |
|---|---|---|
| Any command | 0 | Success |
| Any command | 1 | Error |
wraps email check | 0 | Deliverability grade A or B |
wraps email check | 1 | Deliverability grade C or D |
wraps email check | 2 | Deliverability grade F |
wraps email check | 4 | Check itself failed to run, or grade was unrecognized |
In --json mode, errors are still written to stdout as a JSON envelope — the exit code is what signals failure to scripts:
{ "success": false, "command": "email.init", "error": { "code": "<error code — see below>", "message": "...", "suggestion": "...", "docsUrl": "https://wraps.dev/docs/..." }}@wraps.dev/email)Base class every other email SDK error extends. Use it for a single catch-all instanceof check that covers all of them.
Thrown when an AWS SES API call fails.
| Property | Type | Description |
|---|---|---|
code | string | MessageRejected, Throttling, AccountSuspended, MailFromDomainNotVerified |
requestId | string | AWS request identifier |
retryable | boolean | Whether the request can be retried |
Thrown when an email history read/write operation fails.
| Property | Type | Description |
|---|---|---|
code | string | DynamoDB error code |
requestId | string | AWS request identifier |
retryable | boolean | Whether the request can be retried |
Thrown when input parameters are invalid.
| Property | Type | Description |
|---|---|---|
field | string | Which field failed validation |
message | string | Human-readable error description |
The AWS credential chain produced nothing usable, so no request was ever signed. The message lists every way to supply credentials — SSO, access keys, environment variables, a named profile, or the constructor — without ranking one over another.
| Property | Type | Description |
|---|---|---|
cause | unknown | The underlying AWS SDK error, for debugging |
Extends SESError. SES rejected the send because an identity involved is not verified in the region the request went to. Two unrelated causes produce this one AWS error — a region mismatch and the SES sandbox — so the message names the region actually used and walks through both, including the mailbox-simulator address that proves sending works without production access.
| Property | Type | Description |
|---|---|---|
region | string | undefined | The region this client sent to, when resolvable |
@wraps.dev/sms)Base class every other SMS SDK error extends. Use it for a single catch-all instanceof check that covers all of them.
Thrown when an AWS End User Messaging API call fails.
| Property | Type | Description |
|---|---|---|
code | string | AWS error code |
retryable | boolean | Whether the request can be retried |
Thrown when input parameters are invalid.
| Property | Type | Description |
|---|---|---|
field | string | Which field failed validation |
message | string | Human-readable error description |
Thrown when the recipient has opted out of receiving messages.
| Property | Type | Description |
|---|---|---|
phoneNumber | string | The phone number that opted out |
Thrown when the sending rate limit has been exceeded.
| Property | Type | Description |
|---|---|---|
retryAfter | number | Seconds to wait before retrying |
No Automatic Retries
The SDKs do NOT automatically retry failed requests. If retryable is true, implement your own retry logic using the pattern below.
Use exponential backoff when retrying failed requests. This example uses a simple retry loop with increasing delays.
import { WrapsEmail, SESError } from '@wraps.dev/email';const email = new WrapsEmail();async function sendWithRetry(params, maxRetries = 3) { for (let attempt = 0; attempt < maxRetries; attempt++) { try { return await email.send(params); } catch (error) { if (error instanceof SESError && error.retryable && attempt < maxRetries - 1) { await new Promise(r => setTimeout(r, Math.pow(2, attempt) * 1000)); continue; } throw error; } }}Catch and handle specific error types to provide appropriate responses in your application.
import { WrapsEmail, SESError, DynamoDBError, ValidationError } from '@wraps.dev/email';const email = new WrapsEmail();try { await email.send({ from: 'hello@yourdomain.com', to: 'user@example.com', subject: 'Hello', html: '<p>Hello!</p>', });} catch (error) { if (error instanceof ValidationError) { console.error('Invalid input:', error.field, error.message); } else if (error instanceof SESError) { console.error('SES error:', error.code, error.retryable); } else if (error instanceof DynamoDBError) { console.error('DynamoDB error:', error.code, error.retryable); }}Full API reference for the @wraps.dev/email TypeScript SDK.
View ReferenceFull API reference for the @wraps.dev/sms TypeScript SDK.
View ReferenceConfigure AWS credentials and permissions for Wraps.
View Guide