Suggesting Genesys Cloud Agent Assist Next Best Actions via the Suggest API with Go

Suggesting Genesys Cloud Agent Assist Next Best Actions via the Suggest API with Go

What You Will Build

  • A Go service that constructs, validates, and submits Agent Assist suggestion payloads containing context snapshot references, action score matrices, and display directives.
  • The service interacts with the Genesys Cloud Agent Assist API using the official Go SDK for configuration and standard library HTTP clients for precise payload control.
  • The tutorial covers Go 1.21+ with production-grade error handling, retry logic, latency tracking, and audit logging.

Prerequisites

  • Genesys Cloud OAuth client credentials with the agentassist:suggest scope
  • Go 1.21 or later
  • github.com/mypurecloud/platform-client-sdk-go/v4
  • Standard library packages: context, crypto/tls, encoding/json, fmt, log/slog, net/http, os, strings, sync, time

Authentication Setup

Genesys Cloud requires OAuth 2.0 client credentials flow for server-to-server integrations. The official Go SDK handles token acquisition, caching, and automatic refresh. You must configure the SDK client before issuing any API requests.

package main

import (
	"context"
	"log/slog"
	"os"

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

// InitializeGenesysClient configures the platform client with OAuth credentials.
func InitializeGenesysClient(ctx context.Context) (*platformclientv2.DefaultAPI, error) {
	clientID := os.Getenv("GENESYS_CLIENT_ID")
	clientSecret := os.Getenv("GENESYS_CLIENT_SECRET")
	baseURL := os.Getenv("GENESYS_BASE_URL")

	if clientID == "" || clientSecret == "" {
		return nil, fmt.Errorf("missing GENESYS_CLIENT_ID or GENESYS_CLIENT_SECRET environment variables")
	}

	if baseURL == "" {
		baseURL = "https://api.mypurecloud.com"
	}

	cfg := configuration.NewConfiguration()
	cfg.BaseURL = baseURL
	cfg.ClientId = clientID
	cfg.ClientSecret = clientSecret
	cfg.Scopes = []string{"agentassist:suggest"}

	// The SDK automatically handles token caching and refresh logic.
	client, err := platformclientv2.NewDefaultAPI(cfg)
	if err != nil {
		return nil, fmt.Errorf("failed to initialize Genesys Cloud SDK client: %w", err)
	}

	slog.Info("Genesys Cloud client initialized", "base_url", baseURL)
	return client, nil
}

The SDK maintains an in-memory token cache. When the access token expires, subsequent API calls trigger an automatic refresh using the client credentials grant. You do not need to implement manual token rotation.

Implementation

Step 1: Construct Suggest Payloads with Context Snapshots, Score Matrices, and Display Directives

Genesys Cloud Agent Assist accepts a POST /api/v2/agentassist/suggestions request. The payload must include a conversation identifier, suggestion type, content structure, and optional metadata. You will embed context snapshot references, action score matrices, and display directives inside the metadata field to preserve custom routing logic while remaining compliant with the platform schema.

type ActionScoreMatrix struct {
	RelevanceWeight float64 `json:"relevance_weight"`
	RecencyWeight   float64 `json:"recency_weight"`
	ConfidenceScore float64 `json:"confidence_score"`
}

type DisplayDirective struct {
	Priority      string `json:"priority"`
	CollapseAfter int    `json:"collapse_after_seconds"`
	HighlightTags []string `json:"highlight_tags"`
}

type ContextSnapshotRef struct {
	SnapshotID   string `json:"snapshot_id"`
	TurnIndex    int    `json:"turn_index"`
	ChannelType  string `json:"channel_type"`
}

type SuggestionPayload struct {
	ConversationID string `json:"conversationId"`
	SuggestionType string `json:"suggestionType"`
	Content        any    `json:"content"`
	Score          float64 `json:"score"`
	Metadata       map[string]any `json:"metadata"`
	ExpiresAt      string `json:"expiresAt"`
}

func BuildSuggestionPayload(convID string, snapshot ContextSnapshotRef, matrix ActionScoreMatrix, directive DisplayDirective) SuggestionPayload {
	return SuggestionPayload{
		ConversationID: convID,
		SuggestionType: "article",
		Content: map[string]any{
			"title": "Next Best Action: Resolution Path",
			"text":  "Verify customer account status before proceeding with refund workflow.",
			"url":   "https://kb.example.com/article/12345",
		},
		Score: matrix.ConfidenceScore,
		Metadata: map[string]any{
			"contextSnapshot": snapshot,
			"scoreMatrix":     matrix,
			"displayDirective": directive,
		},
		ExpiresAt: time.Now().Add(5 * time.Minute).UTC().Format(time.RFC3339),
	}
}

The Content field accepts a generic object. You structure it according to the suggestion type. The metadata object carries your custom matrices and directives. Genesys Cloud stores metadata as a flat JSON object, so nested structures serialize correctly.

Step 2: Validate Suggest Schemas Against Assist Engine Constraints

Before submission, you must enforce platform constraints. Genesys Cloud limits concurrent suggestions per conversation and requires scores between 0.0 and 1.0. You will implement a validation pipeline that checks context relevance, action availability, and maximum suggestion counts.

const maxConcurrentSuggestions = 5

type ValidationPipeline struct {
	ContextRelevant   bool
	ActionAvailable   bool
	ScoreNormalized   bool
	WithinCountLimit  bool
}

func RunValidationPipeline(payload SuggestionPayload, currentActiveCount int) (ValidationPipeline, error) {
	pipeline := ValidationPipeline{}

	// Context relevance checking
	if payload.Metadata["contextSnapshot"] == nil {
		pipeline.ContextRelevant = false
	} else {
		pipeline.ContextRelevant = true
	}

	// Action availability verification
	if payload.Content == nil {
		return pipeline, fmt.Errorf("suggestion content cannot be null")
	}
	pipeline.ActionAvailable = true

	// Score normalization trigger
	rawScore := payload.Score
	if rawScore < 0.0 {
		rawScore = 0.0
	}
	if rawScore > 1.0 {
		rawScore = 1.0
	}
	payload.Score = rawScore
	pipeline.ScoreNormalized = true

	// Maximum suggestion count limit
	if currentActiveCount >= maxConcurrentSuggestions {
		pipeline.WithinCountLimit = false
		return pipeline, fmt.Errorf("conversation %s has reached maximum suggestion count limit of %d", payload.ConversationID, maxConcurrentSuggestions)
	}
	pipeline.WithinCountLimit = true

	return pipeline, nil
}

This pipeline returns early if constraints fail. The score normalization step clamps values to the valid range. The count limit prevents 429 Too Many Requests responses from the assist engine.

Step 3: Atomic POST Operations with Format Verification and Retry Logic

You will submit the validated payload using an atomic POST operation. The request must include the Authorization header with a valid bearer token. You will implement exponential backoff for 429 responses and format verification for the response body.

type SuggestionResponse struct {
	ID   string `json:"id"`
	URI  string `json:"uri"`
	Self string `json:"self"`
}

func SubmitSuggestion(ctx context.Context, client *http.Client, baseURL string, token string, payload SuggestionPayload) (SuggestionResponse, error) {
	jsonPayload, err := json.Marshal(payload)
	if err != nil {
		return SuggestionResponse{}, fmt.Errorf("failed to marshal suggestion payload: %w", err)
	}

	url := fmt.Sprintf("%s/api/v2/agentassist/suggestions", baseURL)
	req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewBuffer(jsonPayload))
	if err != nil {
		return SuggestionResponse{}, fmt.Errorf("failed to create request: %w", err)
	}

	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token))

	var resp SuggestionResponse
	var lastErr error

	// Retry logic for 429 rate limits
	for attempt := 0; attempt < 3; attempt++ {
		httpResp, err := client.Do(req)
		if err != nil {
			return resp, fmt.Errorf("HTTP request failed: %w", err)
		}
		defer httpResp.Body.Close()

		bodyBytes, _ := io.ReadAll(httpResp.Body)

		switch httpResp.StatusCode {
		case http.StatusCreated:
			if err := json.Unmarshal(bodyBytes, &resp); err != nil {
				return resp, fmt.Errorf("failed to parse response body: %w", err)
			}
			return resp, nil
		case http.StatusTooManyRequests:
			lastErr = fmt.Errorf("rate limited (429) on attempt %d", attempt+1)
			backoff := time.Duration(attempt+1) * 2 * time.Second
			slog.Warn("Retrying suggestion submission due to 429", "attempt", attempt+1, "backoff", backoff)
			time.Sleep(backoff)
		case http.StatusUnauthorized:
			return resp, fmt.Errorf("authentication failed (401): %s", string(bodyBytes))
		case http.StatusForbidden:
			return resp, fmt.Errorf("insufficient permissions (403): %s", string(bodyBytes))
		default:
			return resp, fmt.Errorf("unexpected status code %d: %s", httpResp.StatusCode, string(bodyBytes))
		}
	}

	return resp, fmt.Errorf("max retries exceeded for suggestion submission: %w", lastErr)
}

The retry loop handles transient rate limits. The function returns immediately on authentication or permission failures. The response body verification ensures the platform accepted the suggestion and returned a valid identifier.

Step 4: Synchronize Events, Track Latency, and Generate Audit Logs

You will wrap the submission in a governance layer that tracks latency, logs audit records, and synchronizes with external knowledge graphs via webhook callbacks.

type AuditLog struct {
	Timestamp    string `json:"timestamp"`
	Conversation string `json:"conversation_id"`
	SuggestionID string `json:"suggestion_id"`
	LatencyMs    float64 `json:"latency_ms"`
	Status       string `json:"status"`
	Pipeline     ValidationPipeline `json:"validation_pipeline"`
}

func SyncKnowledgeGraph(ctx context.Context, graphURL string, suggestionID string) error {
	payload := map[string]any{
		"event": "action_recommended",
		"payload": map[string]any{
			"suggestion_id": suggestionID,
			"timestamp":     time.Now().UTC().Format(time.RFC3339),
		},
	}
	jsonData, _ := json.Marshal(payload)
	req, _ := http.NewRequestWithContext(ctx, http.MethodPost, graphURL, bytes.NewBuffer(jsonData))
	req.Header.Set("Content-Type", "application/json")
	
	client := &http.Client{Timeout: 5 * time.Second}
	resp, err := client.Do(req)
	if err != nil {
		return fmt.Errorf("knowledge graph sync failed: %w", err)
	}
	defer resp.Body.Close()
	
	if resp.StatusCode >= 400 {
		return fmt.Errorf("knowledge graph returned status %d", resp.StatusCode)
	}
	return nil
}

func ProcessSuggestion(ctx context.Context, baseURL string, token string, payload SuggestionPayload, activeCount int, graphSyncURL string) (AuditLog, error) {
	start := time.Now()
	audit := AuditLog{
		Timestamp:    start.UTC().Format(time.RFC3339),
		Conversation: payload.ConversationID,
		Status:       "pending",
	}

	pipeline, err := RunValidationPipeline(payload, activeCount)
	if err != nil {
		audit.Status = "validation_failed"
		audit.Pipeline = pipeline
		return audit, fmt.Errorf("validation failed: %w", err)
	}
	audit.Pipeline = pipeline

	httpClient := &http.Client{
		Transport: &http.Transport{
			TLSClientConfig: &tls.Config{MinVersion: tls.VersionTLS12},
		},
		Timeout: 10 * time.Second,
	}

	resp, err := SubmitSuggestion(ctx, httpClient, baseURL, token, payload)
	latency := time.Since(start).Milliseconds()
	audit.LatencyMs = float64(latency)

	if err != nil {
		audit.Status = "submission_failed"
		return audit, fmt.Errorf("submission failed: %w", err)
	}

	audit.SuggestionID = resp.ID
	audit.Status = "submitted"

	// Synchronize with external knowledge graph
	if graphSyncURL != "" {
		go func() {
			if err := SyncKnowledgeGraph(ctx, graphSyncURL, resp.ID); err != nil {
				slog.Error("Failed to sync knowledge graph", "suggestion_id", resp.ID, "error", err)
			}
		}()
	}

	slog.Info("Suggestion processed successfully", "id", resp.ID, "latency_ms", latency)
	return audit, nil
}

The governance layer executes validation, submission, and synchronization in a deterministic order. Latency tracking uses time.Since(). The webhook synchronization runs asynchronously to avoid blocking the main suggestion flow. Audit logs capture the complete lifecycle for compliance reviews.

Complete Working Example

The following script combines all components into a runnable Go program. It reads credentials from environment variables, constructs a suggestion, validates it, submits it, and outputs the audit record.

package main

import (
	"bytes"
	"context"
	"crypto/tls"
	"encoding/json"
	"fmt"
	"io"
	"log/slog"
	"net/http"
	"os"
	"time"

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

const maxConcurrentSuggestions = 5

type ActionScoreMatrix struct {
	RelevanceWeight float64 `json:"relevance_weight"`
	RecencyWeight   float64 `json:"recency_weight"`
	ConfidenceScore float64 `json:"confidence_score"`
}

type DisplayDirective struct {
	Priority      string `json:"priority"`
	CollapseAfter int    `json:"collapse_after_seconds"`
	HighlightTags []string `json:"highlight_tags"`
}

type ContextSnapshotRef struct {
	SnapshotID   string `json:"snapshot_id"`
	TurnIndex    int    `json:"turn_index"`
	ChannelType  string `json:"channel_type"`
}

type SuggestionPayload struct {
	ConversationID string `json:"conversationId"`
	SuggestionType string `json:"suggestionType"`
	Content        any    `json:"content"`
	Score          float64 `json:"score"`
	Metadata       map[string]any `json:"metadata"`
	ExpiresAt      string `json:"expiresAt"`
}

type SuggestionResponse struct {
	ID   string `json:"id"`
	URI  string `json:"uri"`
	Self string `json:"self"`
}

type ValidationPipeline struct {
	ContextRelevant   bool `json:"context_relevant"`
	ActionAvailable   bool `json:"action_available"`
	ScoreNormalized   bool `json:"score_normalized"`
	WithinCountLimit  bool `json:"within_count_limit"`
}

type AuditLog struct {
	Timestamp    string `json:"timestamp"`
	Conversation string `json:"conversation_id"`
	SuggestionID string `json:"suggestion_id"`
	LatencyMs    float64 `json:"latency_ms"`
	Status       string `json:"status"`
	Pipeline     ValidationPipeline `json:"validation_pipeline"`
}

func BuildSuggestionPayload(convID string, snapshot ContextSnapshotRef, matrix ActionScoreMatrix, directive DisplayDirective) SuggestionPayload {
	return SuggestionPayload{
		ConversationID: convID,
		SuggestionType: "article",
		Content: map[string]any{
			"title": "Next Best Action: Resolution Path",
			"text":  "Verify customer account status before proceeding with refund workflow.",
			"url":   "https://kb.example.com/article/12345",
		},
		Score: matrix.ConfidenceScore,
		Metadata: map[string]any{
			"contextSnapshot": snapshot,
			"scoreMatrix":     matrix,
			"displayDirective": directive,
		},
		ExpiresAt: time.Now().Add(5 * time.Minute).UTC().Format(time.RFC3339),
	}
}

func RunValidationPipeline(payload SuggestionPayload, currentActiveCount int) (ValidationPipeline, error) {
	pipeline := ValidationPipeline{}
	if payload.Metadata["contextSnapshot"] == nil {
		pipeline.ContextRelevant = false
	} else {
		pipeline.ContextRelevant = true
	}
	if payload.Content == nil {
		return pipeline, fmt.Errorf("suggestion content cannot be null")
	}
	pipeline.ActionAvailable = true
	rawScore := payload.Score
	if rawScore < 0.0 {
		rawScore = 0.0
	}
	if rawScore > 1.0 {
		rawScore = 1.0
	}
	payload.Score = rawScore
	pipeline.ScoreNormalized = true
	if currentActiveCount >= maxConcurrentSuggestions {
		pipeline.WithinCountLimit = false
		return pipeline, fmt.Errorf("conversation %s has reached maximum suggestion count limit of %d", payload.ConversationID, maxConcurrentSuggestions)
	}
	pipeline.WithinCountLimit = true
	return pipeline, nil
}

func SubmitSuggestion(ctx context.Context, client *http.Client, baseURL string, token string, payload SuggestionPayload) (SuggestionResponse, error) {
	jsonPayload, err := json.Marshal(payload)
	if err != nil {
		return SuggestionResponse{}, fmt.Errorf("failed to marshal suggestion payload: %w", err)
	}
	url := fmt.Sprintf("%s/api/v2/agentassist/suggestions", baseURL)
	req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewBuffer(jsonPayload))
	if err != nil {
		return SuggestionResponse{}, fmt.Errorf("failed to create request: %w", err)
	}
	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token))
	var resp SuggestionResponse
	var lastErr error
	for attempt := 0; attempt < 3; attempt++ {
		httpResp, err := client.Do(req)
		if err != nil {
			return resp, fmt.Errorf("HTTP request failed: %w", err)
		}
		defer httpResp.Body.Close()
		bodyBytes, _ := io.ReadAll(httpResp.Body)
		switch httpResp.StatusCode {
		case http.StatusCreated:
			if err := json.Unmarshal(bodyBytes, &resp); err != nil {
				return resp, fmt.Errorf("failed to parse response body: %w", err)
			}
			return resp, nil
		case http.StatusTooManyRequests:
			lastErr = fmt.Errorf("rate limited (429) on attempt %d", attempt+1)
			backoff := time.Duration(attempt+1) * 2 * time.Second
			slog.Warn("Retrying suggestion submission due to 429", "attempt", attempt+1, "backoff", backoff)
			time.Sleep(backoff)
		case http.StatusUnauthorized:
			return resp, fmt.Errorf("authentication failed (401): %s", string(bodyBytes))
		case http.StatusForbidden:
			return resp, fmt.Errorf("insufficient permissions (403): %s", string(bodyBytes))
		default:
			return resp, fmt.Errorf("unexpected status code %d: %s", httpResp.StatusCode, string(bodyBytes))
		}
	}
	return resp, fmt.Errorf("max retries exceeded for suggestion submission: %w", lastErr)
}

func SyncKnowledgeGraph(ctx context.Context, graphURL string, suggestionID string) error {
	payload := map[string]any{
		"event": "action_recommended",
		"payload": map[string]any{
			"suggestion_id": suggestionID,
			"timestamp":     time.Now().UTC().Format(time.RFC3339),
		},
	}
	jsonData, _ := json.Marshal(payload)
	req, _ := http.NewRequestWithContext(ctx, http.MethodPost, graphURL, bytes.NewBuffer(jsonData))
	req.Header.Set("Content-Type", "application/json")
	client := &http.Client{Timeout: 5 * time.Second}
	resp, err := client.Do(req)
	if err != nil {
		return fmt.Errorf("knowledge graph sync failed: %w", err)
	}
	defer resp.Body.Close()
	if resp.StatusCode >= 400 {
		return fmt.Errorf("knowledge graph returned status %d", resp.StatusCode)
	}
	return nil
}

func ProcessSuggestion(ctx context.Context, baseURL string, token string, payload SuggestionPayload, activeCount int, graphSyncURL string) (AuditLog, error) {
	start := time.Now()
	audit := AuditLog{
		Timestamp:    start.UTC().Format(time.RFC3339),
		Conversation: payload.ConversationID,
		Status:       "pending",
	}
	pipeline, err := RunValidationPipeline(payload, activeCount)
	if err != nil {
		audit.Status = "validation_failed"
		audit.Pipeline = pipeline
		return audit, fmt.Errorf("validation failed: %w", err)
	}
	audit.Pipeline = pipeline
	httpClient := &http.Client{
		Transport: &http.Transport{
			TLSClientConfig: &tls.Config{MinVersion: tls.VersionTLS12},
		},
		Timeout: 10 * time.Second,
	}
	resp, err := SubmitSuggestion(ctx, httpClient, baseURL, token, payload)
	latency := time.Since(start).Milliseconds()
	audit.LatencyMs = float64(latency)
	if err != nil {
		audit.Status = "submission_failed"
		return audit, fmt.Errorf("submission failed: %w", err)
	}
	audit.SuggestionID = resp.ID
	audit.Status = "submitted"
	if graphSyncURL != "" {
		go func() {
			if err := SyncKnowledgeGraph(ctx, graphSyncURL, resp.ID); err != nil {
				slog.Error("Failed to sync knowledge graph", "suggestion_id", resp.ID, "error", err)
			}
		}()
	}
	slog.Info("Suggestion processed successfully", "id", resp.ID, "latency_ms", latency)
	return audit, nil
}

func main() {
	ctx := context.Background()
	clientID := os.Getenv("GENESYS_CLIENT_ID")
	clientSecret := os.Getenv("GENESYS_CLIENT_SECRET")
	baseURL := os.Getenv("GENESYS_BASE_URL")
	if baseURL == "" {
		baseURL = "https://api.mypurecloud.com"
	}
	graphSyncURL := os.Getenv("KNOWLEDGE_GRAPH_WEBHOOK_URL")

	if clientID == "" || clientSecret == "" {
		slog.Error("Missing required environment variables: GENESYS_CLIENT_ID, GENESYS_CLIENT_SECRET")
		os.Exit(1)
	}

	cfg := configuration.NewConfiguration()
	cfg.BaseURL = baseURL
	cfg.ClientId = clientID
	cfg.ClientSecret = clientSecret
	cfg.Scopes = []string{"agentassist:suggest"}

	// Retrieve a fresh token using the SDK
	accessToken, err := cfg.GetAccessToken(ctx)
	if err != nil {
		slog.Error("Failed to obtain access token", "error", err)
		os.Exit(1)
	}

	snapshot := ContextSnapshotRef{
		SnapshotID:  "snap_8f7d6c5b4a32",
		TurnIndex:   14,
		ChannelType: "voice",
	}
	matrix := ActionScoreMatrix{
		RelevanceWeight: 0.7,
		RecencyWeight:   0.2,
		ConfidenceScore: 0.85,
	}
	directive := DisplayDirective{
		Priority:      "high",
		CollapseAfter: 120,
		HighlightTags: []string{"refund", "verification"},
	}

	payload := BuildSuggestionPayload("conv_9a8b7c6d5e4f", snapshot, matrix, directive)

	audit, err := ProcessSuggestion(ctx, baseURL, accessToken, payload, 2, graphSyncURL)
	if err != nil {
		slog.Error("Suggestion processing failed", "error", err)
	}

	auditJSON, _ := json.MarshalIndent(audit, "", "  ")
	fmt.Println(string(auditJSON))
}

Common Errors & Debugging

Error: 401 Unauthorized

  • Cause: Expired access token, invalid client credentials, or missing agentassist:suggest scope on the OAuth application.
  • Fix: Verify the client ID and secret in the Genesys Cloud admin console. Ensure the OAuth application has the agentassist:suggest scope enabled. Restart the service to trigger a fresh token exchange.
  • Code Check: The SDK automatically refreshes tokens. If the error persists, print the token expiry timestamp and verify system clock synchronization.

Error: 403 Forbidden

  • Cause: The OAuth application lacks organization permissions for Agent Assist, or the user context associated with the token does not have the agentassist:write role.
  • Fix: Navigate to the Genesys Cloud admin console, locate the OAuth application, and assign the Agent Assist permission set. Verify the service account has the Agent or Supervisor role with assist capabilities.
  • Code Check: Log the exact error payload returned by the API. Genesys Cloud includes a reason field that specifies the missing permission.

Error: 429 Too Many Requests

  • Cause: Exceeded the concurrent suggestion limit per conversation or hit global rate limits for the organization.
  • Fix: Implement the exponential backoff retry logic shown in Step 3. Reduce the suggestion generation frequency. Monitor the X-RateLimit-Remaining header in responses to adjust pacing.
  • Code Check: The retry loop sleeps for 2, 4, and 6 seconds across three attempts. If failures continue, increase the backoff multiplier or implement a token bucket rate limiter.

Error: 400 Bad Request

  • Cause: Invalid JSON structure, missing required fields like conversationId or suggestionType, or score values outside the 0.0-1.0 range.
  • Fix: Run the payload through the validation pipeline before submission. Ensure all string fields contain valid UTF-8 characters. Verify the expiresAt field uses RFC3339 format.
  • Code Check: The RunValidationPipeline function clamps scores and checks for null content. Enable debug logging to print the exact JSON payload before transmission.

Official References