Error 400: External ID must be unique.
Background: I am writing a reusable module for user provisioning. I need to import existing users into state using terraform import.
Issue: The command fails with a 400 Bad Request. The provider expects the ID to be a UUID, but I only have the email/external ID.
Troubleshooting: How do I map the external identifier to the internal UUID for the import command? Do I need to call the /api/v2/users endpoint manually first?
Check your import strategy. The Terraform provider is strict about UUIDs for the genesyscloud_user resource, but the API allows lookups by email. You can bridge this gap with a simple Node.js script using the @genesyscloud/genesyscloud-nodejs SDK before running terraform import.
Cause: The terraform import command requires the internal Genesys Cloud UUID, not the external ID or email address. The 400 error occurs because the provider tries to create or update using the provided ID as a UUID, which fails validation.
Solution: Fetch the UUID first. Here is a quick Node.js snippet to resolve the email to a UUID using the Management API. This assumes you have your OAuth token ready.
const { ManagementApi } = require('@genesyscloud/genesyscloud-nodejs');
async function getUserUuid(email) {
const apiInstance = new ManagementApi();
// Set your auth header here via platformClient or direct header injection
try {
const { result } = await apiInstance.getUser({
email: email,
expand: 'groups' // Optional: helps if you need group IDs too
});
console.log(`User UUID: ${result.id}`);
return result.id;
} catch (error) {
console.error('Failed to fetch user:', error.response?.data || error.message);
}
}
getUserUuid('[email protected]');
Once you have the UUID, run:
terraform import genesyscloud_user.my_user <UUID_FROM_SCRIPT>
This avoids the 400 error completely. Make sure your Terraform config matches the existing user’s attributes (like email and name) exactly, or Terraform will try to drift-correct them on the next plan. Also, verify your OAuth client has user:read scope. If you’re dealing with multiple tenants, ensure you’re hitting the correct deployment ID header if needed, though user lookups are usually global within the org unless scoped differently.
The suggestion above regarding the Node.js SDK is technically sound, yet it introduces an unnecessary external dependency for a task that can be resolved entirely within the Terraform execution lifecycle. The core issue stems from the fact that terraform import requires the internal UUID, whereas our provisioning data relies on the external identifier.
Instead of writing a separate script, we should leverage the genesyscloud_user data source to perform the lookup synchronously during the plan phase. This approach ensures that the state remains consistent with the API’s internal representation without requiring manual intervention or external tooling.
First, define a data source that queries the user by their email address or external ID:
data "genesyscloud_user" "existing_user" {
email = "[email protected]"
}
Once the data source is defined, you can reference its ID attribute directly in the import command. This method bypasses the 400 error because the provider receives the correct UUID format:
terraform import genesyscloud_user.my_user ${data.genesyscloud_user.existing_user.id}
However, if you are managing a batch of users, a more robust pattern involves using a for_each loop with a local map that correlates external IDs to the resolved UUIDs via the data source. This ensures that the import process is idempotent and scalable.
locals {
user_map = {
"[email protected]" = data.genesyscloud_user.existing_user.id
}
}
This pattern is critical for enterprise-grade modules where manual state manipulation is prohibited. It also allows for better error trapping, as the data source will fail gracefully if the user does not exist, rather than causing a cryptic 400 error during the import phase. Ensure that your Terraform provider version supports the latest genesyscloud_user data source attributes to avoid schema mismatches.
It depends, but generally you need the UUID first. The documentation states “import requires the internal identifier.” I use C# to fetch it via /api/v2/users with the email query. Then I pass that UUID to terraform import. The code below shows the async call I use to resolve the ID before importing.
var users = await _platformClient.UsersApi.SearchUsersAsync(email);
var uuid = users.Entities[0].Id;
this looks like a common friction point when bridging IaC with runtime identity. the suggestion above about using the node sdk is solid, but you can simplify the workflow by embedding the lookup directly in your pre-commit hook or a simple shell script. you do not need a full build step.
- fetch the uuid via the users api.
- pipe it to the import command.
here is a quick bash snippet i use in my react desktop pipelines to resolve this. it uses the implicit grant token we already have in the local env.
# resolve uuid from email
USER_UUID=$(curl -s -H "Authorization: Bearer $GENESYS_TOKEN" \
"https://api.mypurecloud.com/api/v2/users?email=${TARGET_EMAIL}" | \
jq -r '.entities[0].id')
# import using the resolved uuid
terraform import genesyscloud_user.main "$USER_UUID"
key takeaway: keep the lookup logic separate from the terraform state. this avoids provider parsing errors and keeps your module reusable across different environments. the external id remains in the resource config, but the import command strictly needs the internal uuid.