# Error Codes & Troubleshooting

Source: https://wraps.dev/docs/reference/errors

Complete reference for all CLI error codes and SDK error classes, with solutions for each.

## CLI Error Codes

Copy for AI

### Codes that carry a prefix in `--json`

Most 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.

### Credentials & Session

| 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 |

### IAM & Permissions

| 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 |

### Region & AWS Limits

| 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 |

### Stack & Deployment

| 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 |

### Email / SES

| 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) |

### Tracking Domains

| 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. |

### Inbound Email

| Code | Message | Solution |
| --- | --- | --- |
| `INVALID_FLAG_USAGE` | \--root and --subdomain cannot be used together | Also raised for an unexpected argument, such as a value after a flag that takes none (--root wraps.dev). Use --domain for the parent domain or --subdomain <label> for the label. |
| `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 |

### Reply Threading

| 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 |

### SMS

| 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 |

### SMTP

| 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" |

### Templates & Config

| 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 |

### CLI Usage & Automation

| 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 |

## Exit Codes

Copy for AI

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:

error-envelope.json

```
{  "success": false,  "command": "email.init",  "error": {    "code": "<error code — see below>",    "message": "...",    "suggestion": "...",    "docsUrl": "https://wraps.dev/docs/..."  }}
```

## SDK Error Classes

Copy for AI

### Email SDK (`@wraps.dev/email`)

WrapsEmailError

Base class every other email SDK error extends. Use it for a single catch-all `instanceof` check that covers all of them.

SESError

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 |

DynamoDBError

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 |

ValidationError

Thrown when input parameters are invalid.

| Property | Type | Description |
| --- | --- | --- |
| `field` | string | Which field failed validation |
| `message` | string | Human-readable error description |

CredentialsError

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 |

SandboxError

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 |

### SMS SDK (`@wraps.dev/sms`)

WrapsSMSError

Base class every other SMS SDK error extends. Use it for a single catch-all `instanceof` check that covers all of them.

SMSError

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 |

ValidationError

Thrown when input parameters are invalid.

| Property | Type | Description |
| --- | --- | --- |
| `field` | string | Which field failed validation |
| `message` | string | Human-readable error description |

OptedOutError

Thrown when the recipient has opted out of receiving messages.

| Property | Type | Description |
| --- | --- | --- |
| `phoneNumber` | string | The phone number that opted out |

RateLimitError

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.

## Retry Pattern

Copy for AI

Use exponential backoff when retrying failed requests. This example uses a simple retry loop with increasing delays.

retry-pattern.ts

```
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;    }  }}
```

## Error Handling Example

Copy for AI

Catch and handle specific error types to provide appropriate responses in your application.

error-handling.ts

```
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);  }}
```

## Next Steps

Email SDK

Full API reference for the @wraps.dev/email TypeScript SDK.

[View Reference](https://wraps.dev/docs/sdk-reference)

SMS SDK

Full API reference for the @wraps.dev/sms TypeScript SDK.

[View Reference](https://wraps.dev/docs/sms-sdk-reference)

AWS Setup

Configure AWS credentials and permissions for Wraps.

[View Guide](https://wraps.dev/docs/guides/aws-setup)
