Importing Genesys Cloud Outbound Contact List Records via API with Go

Importing Genesys Cloud Outbound Contact List Records via API with Go

What You Will Build

A production-grade Go service that validates contact data streams, enforces schema constraints, deduplicates records, and submits import payloads to Genesys Cloud Outbound via the /api/v2/outbound/contacts/imports endpoint. The service tracks asynchronous batch processing latency, generates structured audit logs, and triggers CRM synchronization webhooks upon import completion.

Prerequisites

  • OAuth Client Type: Confidential Client (Client Credentials Grant)
  • Required Scopes: outbound:contactlist:write, outbound:contactlist:import:write
  • SDK Version: github.com/mypurecloud/platform-client-sdk-go/v4 (Genesys Cloud Platform Client SDK)
  • Language/Runtime: Go 1.21 or later
  • External Dependencies: Standard library only (net/http, encoding/csv, encoding/json, time, sync, log/slog, os, io)

Authentication Setup

Genesys Cloud uses OAuth 2.0 for API authentication. The Go SDK handles token acquisition and refresh automatically when configured with client credentials. You must initialize the SDK configuration before any API call.

package main

import (
    "context"
    "os"
    "github.com/mypurecloud/platform-client-sdk-go/v4/platformclientv2"
)

func initGenesysClient() (*platformclientv2.Client, error) {
    config := platformclientv2.Configuration{
        BaseURL: "https://api.mypurecloud.com",
    }
    client := platformclientv2.NewConfiguration(config)
    
    client.Credentials = platformclientv2.Credentials{
        ClientId:     os.Getenv("GENESYS_CLIENT_ID"),
        ClientSecret: os.Getenv("GENESYS_CLIENT_SECRET"),
        AuthMethod:   "CLIENT_CREDENTIALS",
    }

    // SDK automatically fetches and caches the access token
    return platformclientv2.NewClient(client.Configuration())
}

The SDK maintains an in-memory token cache. When the token expires, subsequent requests trigger an automatic refresh. You do not need to implement manual token rotation logic.

Implementation

Step 1: Schema Validation and Data Coercion

Genesys Cloud enforces strict data type constraints on contact fields. Invalid payloads trigger 400 Bad Request responses that halt the entire import batch. You must validate and coerce data before submission.

The following function enforces record count limits, validates email and phone formats, and coerces string inputs to expected types.

type ContactRecord struct {
    ID        string
    FirstName string
    LastName  string
    Email     string
    Phone     string
}

type ValidationResult struct {
    Valid   []ContactRecord
    Errors  []ValidationError
    Metrics ValidationMetrics
}

type ValidationError struct {
    Index int
    Field string
    Reason string
}

type ValidationMetrics struct {
    TotalProcessed int
    ValidCount     int
    InvalidCount   int
}

func validateContactStream(records []ContactRecord, maxRecords int) ValidationResult {
    var valid []ContactRecord
    var errors []ValidationError
    
    if len(records) > maxRecords {
        errors = append(errors, ValidationError{
            Index:  -1,
            Field:  "batch_size",
            Reason: fmt.Sprintf("Record count %d exceeds maximum limit %d", len(records), maxRecords),
        })
        return ValidationResult{Errors: errors, Metrics: ValidationMetrics{TotalProcessed: len(records)}}
    }

    for i, r := range records {
        isValid := true
        
        if r.FirstName == "" || len(r.FirstName) > 50 {
            errors = append(errors, ValidationError{Index: i, Field: "firstName", Reason: "Required, max 50 characters"})
            isValid = false
        }
        if r.LastName == "" || len(r.LastName) > 50 {
            errors = append(errors, ValidationError{Index: i, Field: "lastName", Reason: "Required, max 50 characters"})
            isValid = false
        }
        if !isValidEmail(r.Email) {
            errors = append(errors, ValidationError{Index: i, Field: "email", Reason: "Invalid email format"})
            isValid = false
        }
        if !isValidPhone(r.Phone) {
            errors = append(errors, ValidationError{Index: i, Field: "phone", Reason: "Invalid E.164 phone format"})
            isValid = false
        }

        if isValid {
            valid = append(valid, r)
        }
    }

    return ValidationResult{
        Valid: valid,
        Errors: errors,
        Metrics: ValidationMetrics{
            TotalProcessed: len(records),
            ValidCount:     len(valid),
            InvalidCount:   len(errors),
        },
    }
}

func isValidEmail(email string) bool {
    return len(email) > 3 && strings.Contains(email, "@") && strings.Contains(email, ".")
}

func isValidPhone(phone string) bool {
    return strings.HasPrefix(phone, "+") && len(phone) >= 8 && len(phone) <= 15
}

The validation function returns a structured result that separates valid records from validation errors. This prevents partial batch failures and provides precise error reporting for upstream systems.

Step 2: Deduplication and Mapping Matrix Construction

Genesys Cloud supports server-side duplicate detection, but pre-filtering reduces API load and improves import latency. You must construct a field mapping matrix that aligns your source schema with Genesys Cloud target fields.

func deduplicateContacts(records []ContactRecord) []ContactRecord {
    seen := make(map[string]bool)
    var unique []ContactRecord

    for _, r := range records {
        // Deduplicate based on normalized email and phone
        key := strings.ToLower(strings.TrimSpace(r.Email)) + "|" + strings.TrimSpace(r.Phone)
        if !seen[key] {
            seen[key] = true
            unique = append(unique, r)
        }
    }
    return unique
}

func buildMappingMatrix() []platformclientv2.Mappingfield {
    return []platformclientv2.Mappingfield{
        {
            SourceFieldName: platformclientv2.String("id"),
            TargetFieldName: platformclientv2.String("id"),
            TargetFieldType: platformclientv2.String("string"),
        },
        {
            SourceFieldName: platformclientv2.String("firstName"),
            TargetFieldName: platformclientv2.String("firstName"),
            TargetFieldType: platformclientv2.String("string"),
        },
        {
            SourceFieldName: platformclientv2.String("lastName"),
            TargetFieldName: platformclientv2.String("lastName"),
            TargetFieldType: platformclientv2.String("string"),
        },
        {
            SourceFieldName: platformclientv2.String("email"),
            TargetFieldName: platformclientv2.String("email"),
            TargetFieldType: platformclientv2.String("string"),
        },
        {
            SourceFieldName: platformclientv2.String("phone"),
            TargetFieldName: platformclientv2.String("phone"),
            TargetFieldType: platformclientv2.String("string"),
        },
    }
}

func buildDeduplicationDirective() platformclientv2.ImportrequestDuplicateDetection {
    return platformclientv2.ImportrequestDuplicateDetection{
        Action: platformclientv2.String("skip"),
        FieldNames: []string{"email", "phone"},
    }
}

The mapping matrix explicitly declares source-to-target field alignment. Genesys Cloud requires this matrix to parse CSV columns correctly. The deduplication directive instructs the platform to skip records matching existing contacts on the specified fields.

Step 3: Asynchronous Batch Processing and Import Submission

Genesys Cloud processes imports asynchronously. You must generate a CSV payload, host it at a publicly accessible URL, and submit the import request. The API returns immediately with an importId for status polling.

func generateCSV(records []ContactRecord) ([]byte, error) {
    var buf bytes.Buffer
    w := csv.NewWriter(&buf)
    
    // Write header
    if err := w.Write([]string{"id", "firstName", "lastName", "email", "phone"}); err != nil {
        return nil, err
    }
    
    // Write records
    for _, r := range records {
        if err := w.Write([]string{r.ID, r.FirstName, r.LastName, r.Email, r.Phone}); err != nil {
            return nil, err
        }
    }
    w.Flush()
    return buf.Bytes(), w.Error()
}

func submitImportRequest(client *platformclientv2.Client, listID string, fileURL string, importName string) (string, error) {
    mapping := buildMappingMatrix()
    dedupDirective := buildDeduplicationDirective()
    
    importReq := platformclientv2.Importrequest{
        ContactListId:      platformclientv2.String(listID),
        FileUrl:            platformclientv2.String(fileURL),
        Mapping:            &mapping,
        DuplicateDetection: &dedupDirective,
        ImportName:         platformclientv2.String(importName),
    }

    ctx := context.Background()
    api := platformclientv2.NewOutboundApi(client)
    
    // Retry logic for 429 Too Many Requests
    var resp *platformclientv2.Import
    var err error
    for attempt := 0; attempt < 3; attempt++ {
        resp, _, err = api.PostOutboundContactsImports(ctx, importReq)
        if err == nil {
            break
        }
        
        // Handle rate limiting
        if strings.Contains(err.Error(), "429") {
            retryAfter := 2 * (attempt + 1)
            time.Sleep(time.Duration(retryAfter) * time.Second)
            continue
        }
        return "", fmt.Errorf("import submission failed: %w", err)
    }
    
    if resp == nil {
        return "", err
    }
    return *resp.Id, nil
}

The submission function implements exponential backoff for 429 responses. Genesys Cloud enforces strict rate limits on outbound import endpoints. The retry loop prevents cascading failures during high-volume data ingestion.

Step 4: Status Polling, Metrics Tracking, and Webhook Synchronization

Import jobs transition through QUEUED, IN_PROGRESS, COMPLETED, or FAILED states. You must poll the status endpoint, track latency, calculate success rates, and trigger external CRM synchronization upon completion.

type ImportAuditLog struct {
    ImportID     string    `json:"import_id"`
    ContactListID string   `json:"contact_list_id"`
    StartTime    time.Time `json:"start_time"`
    EndTime      time.Time `json:"end_time"`
    DurationMs   int64     `json:"duration_ms"`
    TotalRecords int       `json:"total_records"`
    SuccessCount int       `json:"success_count"`
    ErrorCount   int       `json:"error_count"`
    Status       string    `json:"status"`
    SuccessRate  float64   `json:"success_rate"`
}

func pollAndSync(client *platformclientv2.Client, importID string, listID string, totalRecords int, webhookURL string) error {
    api := platformclientv2.NewOutboundApi(client)
    ctx := context.Background()
    startTime := time.Now()
    
    for {
        time.Sleep(5 * time.Second)
        
        importStatus, _, err := api.GetOutboundContactsImportsImportId(ctx, importID)
        if err != nil {
            return fmt.Errorf("status poll failed: %w", err)
        }
        
        status := *importStatus.Status
        if status == "COMPLETED" || status == "FAILED" {
            endTime := time.Now()
            durationMs := endTime.Sub(startTime).Milliseconds()
            
            // Extract success/error counts from import details
            successCount := 0
            errorCount := 0
            if importStatus.Details != nil {
                successCount = *importStatus.Details.SuccessCount
                errorCount = *importStatus.Details.ErrorCount
            }
            
            successRate := 0.0
            if totalRecords > 0 {
                successRate = float64(successCount) / float64(totalRecords) * 100
            }
            
            auditLog := ImportAuditLog{
                ImportID:      importID,
                ContactListID: listID,
                StartTime:     startTime,
                EndTime:       endTime,
                DurationMs:    durationMs,
                TotalRecords:  totalRecords,
                SuccessCount:  successCount,
                ErrorCount:    errorCount,
                Status:        status,
                SuccessRate:   successRate,
            }
            
            // Generate audit log
            auditJSON, _ := json.MarshalIndent(auditLog, "", "  ")
            slog.Info("Import audit log generated", "log", string(auditJSON))
            
            // Trigger CRM webhook synchronization
            if err := triggerWebhook(webhookURL, auditLog); err != nil {
                slog.Warn("Webhook sync failed", "error", err)
            }
            
            return nil
        }
        
        // Handle 5xx server errors during polling
        if status == "ERROR" {
            return fmt.Errorf("import encountered server error: %s", *importStatus.ErrorMessage)
        }
    }
}

func triggerWebhook(url string, payload any) error {
    body, err := json.Marshal(payload)
    if err != nil {
        return err
    }
    
    resp, err := http.Post(url, "application/json", bytes.NewReader(body))
    if err != nil {
        return err
    }
    defer resp.Body.Close()
    
    if resp.StatusCode >= 400 {
        return fmt.Errorf("webhook returned status %d", resp.StatusCode)
    }
    return nil
}

The polling loop respects Genesys Cloud processing time. The audit log captures operational metrics for compliance and performance tuning. The webhook payload delivers structured import results to external CRM systems for data alignment.

Complete Working Example

package main

import (
    "bytes"
    "context"
    "encoding/csv"
    "encoding/json"
    "fmt"
    "log/slog"
    "net/http"
    "os"
    "strings"
    "time"

    "github.com/mypurecloud/platform-client-sdk-go/v4/platformclientv2"
)

type ContactRecord struct {
    ID        string
    FirstName string
    LastName  string
    Email     string
    Phone     string
}

type ValidationError struct {
    Index  int
    Field  string
    Reason string
}

type ValidationMetrics struct {
    TotalProcessed int
    ValidCount     int
    InvalidCount   int
}

type ValidationResult struct {
    Valid   []ContactRecord
    Errors  []ValidationError
    Metrics ValidationMetrics
}

type ImportAuditLog struct {
    ImportID      string    `json:"import_id"`
    ContactListID string    `json:"contact_list_id"`
    StartTime     time.Time `json:"start_time"`
    EndTime       time.Time `json:"end_time"`
    DurationMs    int64     `json:"duration_ms"`
    TotalRecords  int       `json:"total_records"`
    SuccessCount  int       `json:"success_count"`
    ErrorCount    int       `json:"error_count"`
    Status        string    `json:"status"`
    SuccessRate   float64   `json:"success_rate"`
}

func initGenesysClient() (*platformclientv2.Client, error) {
    config := platformclientv2.Configuration{
        BaseURL: "https://api.mypurecloud.com",
    }
    client := platformclientv2.NewConfiguration(config)
    client.Credentials = platformclientv2.Credentials{
        ClientId:     os.Getenv("GENESYS_CLIENT_ID"),
        ClientSecret: os.Getenv("GENESYS_CLIENT_SECRET"),
        AuthMethod:   "CLIENT_CREDENTIALS",
    }
    return platformclientv2.NewClient(client.Configuration())
}

func validateContactStream(records []ContactRecord, maxRecords int) ValidationResult {
    var valid []ContactRecord
    var errors []ValidationError

    if len(records) > maxRecords {
        errors = append(errors, ValidationError{
            Index:  -1,
            Field:  "batch_size",
            Reason: fmt.Sprintf("Record count %d exceeds maximum limit %d", len(records), maxRecords),
        })
        return ValidationResult{Errors: errors, Metrics: ValidationMetrics{TotalProcessed: len(records)}}
    }

    for i, r := range records {
        isValid := true
        if r.FirstName == "" || len(r.FirstName) > 50 {
            errors = append(errors, ValidationError{Index: i, Field: "firstName", Reason: "Required, max 50 characters"})
            isValid = false
        }
        if r.LastName == "" || len(r.LastName) > 50 {
            errors = append(errors, ValidationError{Index: i, Field: "lastName", Reason: "Required, max 50 characters"})
            isValid = false
        }
        if !isValidEmail(r.Email) {
            errors = append(errors, ValidationError{Index: i, Field: "email", Reason: "Invalid email format"})
            isValid = false
        }
        if !isValidPhone(r.Phone) {
            errors = append(errors, ValidationError{Index: i, Field: "phone", Reason: "Invalid E.164 phone format"})
            isValid = false
        }
        if isValid {
            valid = append(valid, r)
        }
    }

    return ValidationResult{
        Valid:  valid,
        Errors: errors,
        Metrics: ValidationMetrics{
            TotalProcessed: len(records),
            ValidCount:     len(valid),
            InvalidCount:   len(errors),
        },
    }
}

func isValidEmail(email string) bool {
    return len(email) > 3 && strings.Contains(email, "@") && strings.Contains(email, ".")
}

func isValidPhone(phone string) bool {
    return strings.HasPrefix(phone, "+") && len(phone) >= 8 && len(phone) <= 15
}

func deduplicateContacts(records []ContactRecord) []ContactRecord {
    seen := make(map[string]bool)
    var unique []ContactRecord
    for _, r := range records {
        key := strings.ToLower(strings.TrimSpace(r.Email)) + "|" + strings.TrimSpace(r.Phone)
        if !seen[key] {
            seen[key] = true
            unique = append(unique, r)
        }
    }
    return unique
}

func buildMappingMatrix() []platformclientv2.Mappingfield {
    return []platformclientv2.Mappingfield{
        {SourceFieldName: platformclientv2.String("id"), TargetFieldName: platformclientv2.String("id"), TargetFieldType: platformclientv2.String("string")},
        {SourceFieldName: platformclientv2.String("firstName"), TargetFieldName: platformclientv2.String("firstName"), TargetFieldType: platformclientv2.String("string")},
        {SourceFieldName: platformclientv2.String("lastName"), TargetFieldName: platformclientv2.String("lastName"), TargetFieldType: platformclientv2.String("string")},
        {SourceFieldName: platformclientv2.String("email"), TargetFieldName: platformclientv2.String("email"), TargetFieldType: platformclientv2.String("string")},
        {SourceFieldName: platformclientv2.String("phone"), TargetFieldName: platformclientv2.String("phone"), TargetFieldType: platformclientv2.String("string")},
    }
}

func buildDeduplicationDirective() platformclientv2.ImportrequestDuplicateDetection {
    return platformclientv2.ImportrequestDuplicateDetection{
        Action:     platformclientv2.String("skip"),
        FieldNames: []string{"email", "phone"},
    }
}

func generateCSV(records []ContactRecord) ([]byte, error) {
    var buf bytes.Buffer
    w := csv.NewWriter(&buf)
    if err := w.Write([]string{"id", "firstName", "lastName", "email", "phone"}); err != nil {
        return nil, err
    }
    for _, r := range records {
        if err := w.Write([]string{r.ID, r.FirstName, r.LastName, r.Email, r.Phone}); err != nil {
            return nil, err
        }
    }
    w.Flush()
    return buf.Bytes(), w.Error()
}

func submitImportRequest(client *platformclientv2.Client, listID string, fileURL string, importName string) (string, error) {
    mapping := buildMappingMatrix()
    dedupDirective := buildDeduplicationDirective()
    importReq := platformclientv2.Importrequest{
        ContactListId:      platformclientv2.String(listID),
        FileUrl:            platformclientv2.String(fileURL),
        Mapping:            &mapping,
        DuplicateDetection: &dedupDirective,
        ImportName:         platformclientv2.String(importName),
    }

    ctx := context.Background()
    api := platformclientv2.NewOutboundApi(client)

    var resp *platformclientv2.Import
    var err error
    for attempt := 0; attempt < 3; attempt++ {
        resp, _, err = api.PostOutboundContactsImports(ctx, importReq)
        if err == nil {
            break
        }
        if strings.Contains(err.Error(), "429") {
            retryAfter := 2 * (attempt + 1)
            time.Sleep(time.Duration(retryAfter) * time.Second)
            continue
        }
        return "", fmt.Errorf("import submission failed: %w", err)
    }

    if resp == nil {
        return "", err
    }
    return *resp.Id, nil
}

func triggerWebhook(url string, payload any) error {
    body, err := json.Marshal(payload)
    if err != nil {
        return err
    }
    resp, err := http.Post(url, "application/json", bytes.NewReader(body))
    if err != nil {
        return err
    }
    defer resp.Body.Close()
    if resp.StatusCode >= 400 {
        return fmt.Errorf("webhook returned status %d", resp.StatusCode)
    }
    return nil
}

func pollAndSync(client *platformclientv2.Client, importID string, listID string, totalRecords int, webhookURL string) error {
    api := platformclientv2.NewOutboundApi(client)
    ctx := context.Background()
    startTime := time.Now()

    for {
        time.Sleep(5 * time.Second)
        importStatus, _, err := api.GetOutboundContactsImportsImportId(ctx, importID)
        if err != nil {
            return fmt.Errorf("status poll failed: %w", err)
        }

        status := *importStatus.Status
        if status == "COMPLETED" || status == "FAILED" {
            endTime := time.Now()
            durationMs := endTime.Sub(startTime).Milliseconds()
            successCount := 0
            errorCount := 0
            if importStatus.Details != nil {
                successCount = *importStatus.Details.SuccessCount
                errorCount = *importStatus.Details.ErrorCount
            }
            successRate := 0.0
            if totalRecords > 0 {
                successRate = float64(successCount) / float64(totalRecords) * 100
            }
            auditLog := ImportAuditLog{
                ImportID:      importID,
                ContactListID: listID,
                StartTime:     startTime,
                EndTime:       endTime,
                DurationMs:    durationMs,
                TotalRecords:  totalRecords,
                SuccessCount:  successCount,
                ErrorCount:    errorCount,
                Status:        status,
                SuccessRate:   successRate,
            }
            auditJSON, _ := json.MarshalIndent(auditLog, "", "  ")
            slog.Info("Import audit log generated", "log", string(auditJSON))
            if err := triggerWebhook(webhookURL, auditLog); err != nil {
                slog.Warn("Webhook sync failed", "error", err)
            }
            return nil
        }
        if status == "ERROR" {
            return fmt.Errorf("import encountered server error: %s", *importStatus.ErrorMessage)
        }
    }
}

func main() {
    client, err := initGenesysClient()
    if err != nil {
        slog.Error("Failed to initialize Genesys client", "error", err)
        os.Exit(1)
    }

    // Sample data stream
    records := []ContactRecord{
        {ID: "C001", FirstName: "Alice", LastName: "Smith", Email: "alice@example.com", Phone: "+15551234567"},
        {ID: "C002", FirstName: "Bob", LastName: "Jones", Email: "bob@example.com", Phone: "+15559876543"},
        {ID: "C001", FirstName: "Alice", LastName: "Smith", Email: "alice@example.com", Phone: "+15551234567"}, // Duplicate
        {ID: "C003", FirstName: "Charlie", LastName: "Invalid", Email: "bad-email", Phone: "+15551111111"}, // Invalid
    }

    // Step 1: Validate
    result := validateContactStream(records, 100000)
    if len(result.Errors) > 0 {
        slog.Warn("Validation errors detected", "errors", result.Errors)
    }

    // Step 2: Deduplicate
    uniqueRecords := deduplicateContacts(result.Valid)

    // Step 3: Generate CSV and host temporarily
    csvData, err := generateCSV(uniqueRecords)
    if err != nil {
        slog.Error("CSV generation failed", "error", err)
        os.Exit(1)
    }

    // Simulate file hosting (in production, upload to S3/Azure Blob with presigned URL)
    server := http.NewServeMux()
    server.HandleFunc("/contacts.csv", func(w http.ResponseWriter, r *http.Request) {
        w.Header().Set("Content-Type", "text/csv")
        w.Write(csvData)
    })
    go http.ListenAndServe(":8080", server)
    fileURL := "http://localhost:8080/contacts.csv"

    // Step 4: Submit Import
    listID := os.Getenv("GENESYS_CONTACT_LIST_ID")
    importName := fmt.Sprintf("outbound_import_%d", time.Now().Unix())
    importID, err := submitImportRequest(client, listID, fileURL, importName)
    if err != nil {
        slog.Error("Import submission failed", "error", err)
        os.Exit(1)
    }
    slog.Info("Import submitted", "importId", importID)

    // Step 5: Poll, Track, and Sync
    webhookURL := os.Getenv("CRM_WEBHOOK_URL")
    if err := pollAndSync(client, importID, listID, len(uniqueRecords), webhookURL); err != nil {
        slog.Error("Import sync failed", "error", err)
    }
}

Common Errors and Debugging

Error: 400 Bad Request (Invalid Mapping or Schema Mismatch)

  • Cause: The CSV header does not match the mapping array source fields, or data types violate Genesys Cloud constraints.
  • Fix: Verify that every SourceFieldName in the mapping matrix exactly matches the CSV header. Ensure string fields do not exceed 255 characters. Validate phone numbers against E.164 format before submission.
  • Code Fix: Add header validation in generateCSV and cross-reference with buildMappingMatrix().

Error: 401 Unauthorized (Token Expired or Invalid Scopes)

  • Cause: OAuth token expired during long-running polling loops, or the client lacks outbound:contactlist:import:write.
  • Fix: The SDK handles automatic refresh. Verify environment variables contain valid credentials. Ensure the OAuth client in Genesys Cloud admin console has the required scopes assigned.
  • Code Fix: Check client.Credentials initialization. Log SDK authentication errors explicitly.

Error: 429 Too Many Requests (Rate Limit Cascade)

  • Cause: Exceeding Genesys Cloud API rate limits during bulk submissions or aggressive polling.
  • Fix: Implement exponential backoff. The example includes a retry loop with 2 * (attempt + 1) second delays. Reduce polling frequency to 5 seconds minimum.
  • Code Fix: Monitor Retry-After headers in HTTP responses. Adjust sleep duration dynamically.

Error: 500 Internal Server Error (Import Processing Failure)

  • Cause: Corrupted CSV encoding, unsupported characters, or platform-side storage limits.
  • Fix: Ensure CSV uses UTF-8 encoding. Strip control characters. Verify file size does not exceed 50MB. Check import details endpoint for ErrorMessage.
  • Code Fix: Add strings.TrimSpace and character filtering before CSV generation. Parse importStatus.ErrorMessage during polling.

Official References