Creating Genesys Cloud Survey Definitions via the Survey API with C#

Creating Genesys Cloud Survey Definitions via the Survey API with C#

What You Will Build

  • A production-grade C# service that constructs, validates, and atomically creates survey definitions using the Genesys Cloud Survey API.
  • The implementation leverages the GenesysCloudPlatformSDK to execute POST /api/v2/surveys with logic branching, distribution channels, and engine constraint enforcement.
  • The code runs on .NET 8, includes retry logic for rate limits, tracks latency and success metrics, emits audit logs, and exposes callback handlers for marketing automation synchronization.

Prerequisites

  • OAuth 2.0 Service Account with survey:write scope
  • Genesys Cloud Platform SDK v3.0+ (GenesysCloudPlatformSDK NuGet package)
  • .NET 8 SDK or compatible runtime
  • Serilog and Serilog.Sinks.Console NuGet packages for structured audit logging
  • System.Text.Json (included in .NET 8)

Authentication Setup

The Genesys Cloud SDK manages token lifecycle automatically when initialized with a Service Account. The following configuration establishes a cached PlatformClient instance that handles token refresh transparently.

using GenesysCloudPlatformSDK;
using GenesysCloudPlatformSDK.Client;
using System;
using System.Threading.Tasks;

public static class GenesysAuth
{
    private static PlatformClient _platformClient;

    public static async Task<PlatformClient> InitializeAsync(
        string clientId,
        string clientSecret,
        string environmentUrl)
    {
        _platformClient = new PlatformClient();
        await _platformClient.Auth.SetTokenAsync(clientId, clientSecret, $"{environmentUrl}/oauth/token");
        return _platformClient;
    }

    public static PlatformClient GetClient()
    {
        _platformClient ??= throw new InvalidOperationException("PlatformClient has not been initialized.");
        return _platformClient;
    }
}

The SetTokenAsync method stores the bearer token in memory and automatically refreshes it before expiration. The SDK attaches the Authorization: Bearer <token> header to every subsequent API call.

Implementation

Step 1: Payload Construction and Validation Pipeline

Survey creation fails when payloads violate engine constraints. The validation pipeline verifies required fields, enforces maximum question counts, validates response types, and ensures logic branch matrices reference valid question identifiers.

using GenesysCloudPlatformSDK.Models;
using System;
using System.Collections.Generic;
using System.Linq;

public class SurveyValidationException : Exception
{
    public SurveyValidationException(string message) : base(message) { }
}

public static class SurveyPayloadValidator
{
    private const int MaxAllowedQuestions = 100;
    private static readonly HashSet<string> ValidResponseTypes = new() 
    { "singleSelect", "multiSelect", "text", "rating", "netPromoter", "date", "email" };
    private static readonly HashSet<string> ValidChannels = new() 
    { "email", "sms", "voice", "webchat", "facebook", "whatsapp" };

    public static void Validate(CreateSurveyRequest request)
    {
        if (string.IsNullOrWhiteSpace(request.Name))
            throw new SurveyValidationException("Survey name is required and cannot be empty.");

        var totalQuestions = (request.Questions?.Count ?? 0) + (request.QuestionSets?.Count ?? 0);
        if (totalQuestions > MaxAllowedQuestions)
            throw new SurveyValidationException($"Question count ({totalQuestions}) exceeds maximum limit ({MaxAllowedQuestions}).");

        if (request.Questions != null)
        {
            for (int i = 0; i < request.Questions.Count; i++)
            {
                var q = request.Questions[i];
                if (!ValidResponseTypes.Contains(q.ResponseType))
                    throw new SurveyValidationException($"Invalid response type '{q.ResponseType}' at question index {i}.");
                
                if (string.IsNullOrWhiteSpace(q.Text))
                    throw new SurveyValidationException($"Question text is required at index {i}.");
            }
        }

        if (request.Logic != null)
        {
            var validQuestionIds = request.Questions?.Select(q => q.Id).ToHashSet() ?? new HashSet<string>();
            foreach (var rule in request.Logic)
            {
                if (!string.IsNullOrEmpty(rule.QuestionId) && !validQuestionIds.Contains(rule.QuestionId))
                    throw new SurveyValidationException($"Logic rule references invalid question ID '{rule.QuestionId}'.");
            }
        }

        if (request.DistributionChannels != null)
        {
            foreach (var channel in request.DistributionChannels)
            {
                if (!ValidChannels.Contains(channel.ToLowerInvariant()))
                    throw new SurveyValidationException($"Unsupported distribution channel '{channel}'.");
            }
        }
    }
}

This validator runs synchronously before the HTTP request. It prevents 400 Bad Request responses by catching schema violations early. The logic branch matrix validation ensures routing rules point to existing question identifiers.

Step 2: Atomic POST Operation with Format Verification and Retry Logic

The Survey API requires a single atomic POST /api/v2/surveys call. The SDK method CreateSurveyAsync handles serialization, but production code must implement exponential backoff for 429 Too Many Requests responses.

using GenesysCloudPlatformSDK;
using GenesysCloudPlatformSDK.Client;
using GenesysCloudPlatformSDK.Models;
using System;
using System.Net;
using System.Threading.Tasks;

public class SurveyCreationResult
{
    public string SurveyId { get; set; }
    public string SelfUri { get; set; }
    public double LatencyMs { get; set; }
    public bool Success { get; set; }
    public string ErrorMessage { get; set; }
    public DateTime CreatedAt { get; set; }
}

public static class SurveyApiClient
{
    private static readonly PlatformClient PlatformClient = GenesysAuth.GetClient();
    private const int MaxRetries = 3;
    private static readonly Random _random = new Random();

    public static async Task<SurveyCreationResult> CreateSurveyAsync(CreateSurveyRequest request)
    {
        var stopwatch = System.Diagnostics.Stopwatch.StartNew();
        SurveyCreationResult result = new() { CreatedAt = DateTime.UtcNow };

        try
        {
            SurveyPayloadValidator.Validate(request);
            request.Preview = true;
            request.Enabled = true;

            for (int attempt = 0; attempt <= MaxRetries; attempt++)
            {
                try
                {
                    // POST /api/v2/surveys
                    var response = await PlatformClient.Survey.CreateSurveyAsync(request);
                    stopwatch.Stop();

                    result.SurveyId = response.Id;
                    result.SelfUri = response.SelfUri;
                    result.LatencyMs = stopwatch.Elapsed.TotalMilliseconds;
                    result.Success = true;
                    return result;
                }
                catch (ApiResponseException ex) when (ex.StatusCode == (int)HttpStatusCode.TooManyRequests && attempt < MaxRetries)
                {
                    int delay = (int)Math.Pow(2, attempt) * 500 + _random.Next(0, 500);
                    await Task.Delay(delay);
                }
                catch (ApiResponseException ex)
                {
                    stopwatch.Stop();
                    result.Success = false;
                    result.ErrorMessage = $"API Error {ex.StatusCode}: {ex.Message}";
                    throw;
                }
            }

            result.Success = false;
            result.ErrorMessage = "Exceeded maximum retry attempts for rate limiting.";
            return result;
        }
        catch (SurveyValidationException ex)
        {
            stopwatch.Stop();
            result.Success = false;
            result.ErrorMessage = $"Validation failed: {ex.Message}";
            return result;
        }
    }
}

The request.Preview = true flag triggers automatic preview generation within the survey engine. The retry loop catches ApiResponseException with status 429, applies jittered exponential backoff, and preserves latency metrics. The method returns a structured result object for downstream processing.

Step 3: Callback Synchronization, Latency Tracking, and Audit Logging

External marketing automation tools require event synchronization. The creator service exposes a callback handler, tracks success rates, and emits structured audit logs for governance compliance.

using System;
using System.Collections.Concurrent;
using System.Text.Json;
using System.Threading.Tasks;

public class SurveyCreatorMetrics
{
    public long TotalAttempts { get; private set; }
    public long SuccessfulCreations { get; private set; }
    public double AverageLatencyMs { get; private set; }
    private double _latencySum;

    public void RecordResult(SurveyCreationResult result)
    {
        TotalAttempts++;
        if (result.Success)
        {
            SuccessfulCreations++;
            _latencySum += result.LatencyMs;
            AverageLatencyMs = _latencySum / SuccessfulCreations;
        }
    }

    public double GetSuccessRate() => TotalAttempts > 0 ? (double)SuccessfulCreations / TotalAttempts : 0.0;
}

public class AutomatedSurveyCreator
{
    private readonly SurveyCreatorMetrics _metrics = new();
    private readonly Action<SurveyCreationResult> _marketingCallback;
    private static readonly JsonSerializerOptions _jsonOptions = new() { WriteIndented = true };

    public AutomatedSurveyCreator(Action<SurveyCreationResult> marketingCallback)
    {
        _marketingCallback = marketingCallback ?? throw new ArgumentNullException(nameof(marketingCallback));
    }

    public async Task ExecuteCreationAsync(CreateSurveyRequest surveyRequest)
    {
        var result = await SurveyApiClient.CreateSurveyAsync(surveyRequest);
        _metrics.RecordResult(result);

        if (result.Success)
        {
            _marketingCallback(result);
            LogAuditEntry(result, surveyRequest, "CREATED");
        }
        else
        {
            LogAuditEntry(result, surveyRequest, "FAILED");
        }
    }

    private void LogAuditEntry(SurveyCreationResult result, CreateSurveyRequest request, string action)
    {
        var auditPayload = new
        {
            timestamp = DateTime.UtcNow.ToString("O"),
            action = action,
            surveyId = result.SurveyId,
            surveyName = request.Name,
            latencyMs = result.LatencyMs,
            success = result.Success,
            errorMessage = result.ErrorMessage,
            distributionChannels = request.DistributionChannels,
            questionCount = (request.Questions?.Count ?? 0) + (request.QuestionSets?.Count ?? 0),
            metrics = new
            {
                totalAttempts = _metrics.TotalAttempts,
                successRate = _metrics.GetSuccessRate(),
                averageLatencyMs = _metrics.AverageLatencyMs
            }
        };

        string jsonLog = JsonSerializer.Serialize(auditPayload, _jsonOptions);
        Console.WriteLine($"[AUDIT] {jsonLog}");
    }

    public SurveyCreatorMetrics GetMetrics() => _metrics;
}

The callback handler receives the creation result and can forward it to external systems via HTTP, message queues, or SDKs. The audit log includes latency, success state, distribution channels, and aggregate metrics. All logging uses ISO 8601 timestamps and structured JSON for parser compatibility.

Complete Working Example

The following console application demonstrates end-to-end survey creation. Replace the placeholder credentials with your Service Account values.

using GenesysCloudPlatformSDK.Models;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;

class Program
{
    static async Task Main(string[] args)
    {
        string clientId = "YOUR_CLIENT_ID";
        string clientSecret = "YOUR_CLIENT_SECRET";
        string environmentUrl = "https://api.mypurecloud.com";

        await GenesysAuth.InitializeAsync(clientId, clientSecret, environmentUrl);

        Action<SurveyCreationResult> marketingSyncHandler = (result) =>
        {
            if (result.Success)
            {
                Console.WriteLine($"[MARKETING SYNC] Survey {result.SurveyId} created. Triggering campaign alignment.");
            }
            else
            {
                Console.WriteLine($"[MARKETING SYNC] Creation failed. Skipping alignment. Error: {result.ErrorMessage}");
            }
        };

        var creator = new AutomatedSurveyCreator(marketingSyncHandler);

        var surveyRequest = new CreateSurveyRequest
        {
            Name = "Q3 Customer Experience Pulse",
            Description = "Quarterly satisfaction and NPS tracking survey",
            Preview = true,
            Enabled = true,
            MaxQuestions = 100,
            DistributionChannels = new List<string> { "email", "webchat", "sms" },
            Questions = new List<Question>
            {
                new()
                {
                    Id = "q1_nps",
                    Text = "How likely are you to recommend our service to a colleague?",
                    ResponseType = "netPromoter",
                    Required = true
                },
                new()
                {
                    Id = "q2_feedback",
                    Text = "What can we improve?",
                    ResponseType = "text",
                    Required = false
                }
            },
            Logic = new List<LogicRule>
            {
                new()
                {
                    QuestionId = "q1_nps",
                    Condition = "lessThan",
                    Value = "7",
                    Action = "skipTo",
                    TargetQuestionId = "q2_feedback"
                }
            },
            QuestionSets = new List<QuestionSetReference>
            {
                new() { Id = "csat_standard_set_v2" }
            }
        };

        try
        {
            await creator.ExecuteCreationAsync(surveyRequest);
            var metrics = creator.GetMetrics();
            Console.WriteLine($"Final Success Rate: {metrics.GetSuccessRate():P2}");
            Console.WriteLine($"Average Latency: {metrics.AverageLatencyMs:F2} ms");
        }
        catch (Exception ex)
        {
            Console.WriteLine($"Fatal execution error: {ex.Message}");
        }
    }
}

Run the application with dotnet run. The output displays audit logs, callback execution, and aggregate metrics. The survey appears in the Genesys Cloud console with preview generation enabled and distribution channels configured.

Common Errors & Debugging

Error: 400 Bad Request

  • Cause: Payload violates schema constraints, missing required fields, or logic references invalid question IDs.
  • Fix: The validation pipeline catches these before the HTTP call. Review the SurveyValidationException message. Ensure Question.Id values match exactly across Questions, Logic, and QuestionSets.
  • Code: The SurveyPayloadValidator.Validate method throws descriptive exceptions. Wrap the call in a try-catch to capture the specific field failure.

Error: 401 Unauthorized

  • Cause: OAuth token expired, client credentials incorrect, or survey:write scope missing.
  • Fix: Verify the Service Account has the survey:write scope assigned in the Genesys Cloud admin console. Regenerate the client secret if rotated. The SDK SetTokenAsync will throw if authentication fails.
  • Code: Check ApiResponseException.StatusCode == 401 and reinitialize GenesysAuth with updated credentials.

Error: 403 Forbidden

  • Cause: Service Account lacks organizational permissions or survey creation is restricted by role policies.
  • Fix: Assign the Survey Creator or Survey Manager role to the Service Account. Verify organization-level feature flags are enabled.
  • Code: Log the response body from ApiResponseException.Content to identify the specific policy violation.

Error: 409 Conflict

  • Cause: A survey with the same name and identical configuration already exists, or a name collision occurs within the same workspace.
  • Fix: Append a timestamp or UUID to the Name field during automated runs. Query existing surveys via GET /api/v2/surveys before creation if idempotency is required.
  • Code: Implement a pre-flight check: var existing = await PlatformClient.Survey.SearchSurveysAsync(name: surveyName);

Error: 429 Too Many Requests

  • Cause: Rate limit exceeded on the Survey API endpoint.
  • Fix: The retry logic in SurveyApiClient.CreateSurveyAsync handles this automatically with exponential backoff and jitter. Reduce creation frequency if cascading failures occur.
  • Code: Monitor the Retry-After header if custom HTTP clients are used. The SDK abstracts this, but logging the attempt count helps identify throttling patterns.

Official References