Compressing Genesys Cloud Purge Archive Manifests with Go

Compressing Genesys Cloud Purge Archive Manifests with Go

What You Will Build

  • A Go service that constructs, validates, and submits compression-optimized purge archive manifests to Genesys Cloud CX.
  • The implementation uses the official genesyscloud-sdk-go client and the /api/v2/purge/requests endpoint.
  • The tutorial covers Go 1.21+ with production-grade error handling, retry logic, metric tracking, and webhook synchronization.

Prerequisites

  • Genesys Cloud CX OAuth confidential client with scopes: purge:read, purge:write, archivestorage:read
  • github.com/mygenesys/genesyscloud-sdk-go v1.26.0 or later
  • Go 1.21+ runtime
  • External dependencies: github.com/google/uuid, golang.org/x/oauth2, golang.org/x/time/rate

Authentication Setup

Genesys Cloud CX uses OAuth 2.0 client credentials for service-to-service automation. The SDK requires a configured PlatformClient with a valid token source. The following code demonstrates token acquisition, caching, and SDK initialization.

package main

import (
	"context"
	"fmt"
	"log"
	"os"
	"time"

	"github.com/mygenesys/genesyscloud-sdk-go/platformclientv2"
	"golang.org/x/oauth2"
	"golang.org/x/oauth2/clientcredentials"
)

func initGenesysClient(clientID, clientSecret, envName string) (*platformclientv2.PlatformClient, error) {
	cfg := &clientcredentials.Config{
		ClientID:     clientID,
		ClientSecret: clientSecret,
		TokenURL:     fmt.Sprintf("https://api.%s.mygenesys.com/oauth/token", envName),
		Scopes:       []string{"purge:read", "purge:write", "archivestorage:read"},
	}

	ctx := context.Background()
	tokenSource := cfg.TokenSource(ctx)
	
	// SDK initialization
	config := platformclientv2.Configuration{
		Environment: platformclientv2.PureCloudEnv(envName),
		OAuthToken:  tokenSource,
	}

	client, err := platformclientv2.NewPlatformClient(config)
	if err != nil {
		return nil, fmt.Errorf("failed to initialize platform client: %w", err)
	}

	return client, nil
}

The TokenURL must match your deployment region (us-east-1, eu-west-1, au-southeast-1, etc.). The clientcredentials package handles token refresh automatically when the SDK makes subsequent API calls.

Implementation

Step 1: Construct and Validate Compression Payloads

The purge engine enforces strict schema constraints. You must validate retention policy references, codec selection, compression ratio directives, and maximum archive size limits before submission. Genesys Cloud limits single archive exports to 2 GB (2,147,483,648 bytes).

type CompressionConfig struct {
	RetentionPolicyID string
	CompressionCodec  string // "gzip", "deflate", "none"
	TargetRatio       float32
	MaxArchiveSizeMB  int
	Description       string
	EntityID          string
}

func validateCompressionConfig(cfg CompressionConfig) error {
	// Validate codec matrix against purge engine constraints
	validCodecs := map[string]bool{"gzip": true, "deflate": true, "none": true}
	if !validCodecs[cfg.CompressionCodec] {
		return fmt.Errorf("invalid compression codec: %s. Must be gzip, deflate, or none", cfg.CompressionCodec)
	}

	// Validate maximum archive size limit (2GB = 2048 MB)
	if cfg.MaxArchiveSizeMB > 2048 {
		return fmt.Errorf("max archive size %d MB exceeds purge engine limit of 2048 MB", cfg.MaxArchiveSizeMB)
	}

	// Validate ratio directive (0.0 to 1.0)
	if cfg.TargetRatio < 0.0 || cfg.TargetRatio > 1.0 {
		return fmt.Errorf("compression ratio must be between 0.0 and 1.0, got %f", cfg.TargetRatio)
	}

	// Validate retention policy reference format (UUID)
	if _, err := uuid.Parse(cfg.RetentionPolicyID); err != nil {
		return fmt.Errorf("invalid retention policy ID format: %w", err)
	}

	return nil
}

The validation function prevents schema rejection at the API layer. The purge engine rejects payloads with unsupported codecs or oversized limits before allocation.

Step 2: Atomic POST Submission with Format Verification

You submit the manifest using an atomic POST operation. The SDK constructs the request with Accept-Encoding: gzip and Content-Type: application/json headers automatically. You must handle HTTP status codes explicitly and implement retry logic for rate limits.

type ManifestCompressor struct {
	purgeAPI *platformclientv2.PurgeApi
	config   CompressionConfig
}

func NewManifestCompressor(client *platformclientv2.PlatformClient, cfg CompressionConfig) *ManifestCompressor {
	return &ManifestCompressor{
		purgeAPI: client.NewPurgeApi(),
		config:   cfg,
	}
}

func (mc *ManifestCompressor) SubmitManifest(ctx context.Context) (*platformclientv2.PurgeRequest, error) {
	// Build SDK payload
	purgeReq := platformclientv2.PurgeRequest{
		RetentionPolicyId:     &mc.config.RetentionPolicyID,
		Description:           &mc.config.Description,
		EntityId:              &mc.config.EntityID,
		PurgeRequestType:      platformclientv2.PtrString("conversations"),
		ArchiveCompressionType: platformclientv2.PtrString(mc.config.CompressionCodec),
		MaxArchiveSize:        platformclientv2.PtrInt64(int64(mc.config.MaxArchiveSizeMB * 1024 * 1024)),
	}

	// Atomic POST with retry logic for 429
	var response *platformclientv2.PurgeRequest
	var err error
	maxRetries := 3

	for attempt := 0; attempt <= maxRetries; attempt++ {
		response, _, err = mc.purgeAPI.CreatePurgeRequest(ctx, purgeReq)
		if err == nil {
			break
		}

		// Parse SDK error for HTTP status
		if apiErr, ok := err.(*platformclientv2.ApiError); ok {
			if apiErr.GetStatusCode() == 429 {
				waitTime := time.Duration(1<<uint(attempt)) * time.Second
				log.Printf("Rate limited (429). Retrying in %v...", waitTime)
				time.Sleep(waitTime)
				continue
			}
			if apiErr.GetStatusCode() == 401 || apiErr.GetStatusCode() == 403 {
				return nil, fmt.Errorf("authentication/authorization failed: status %d, message %s", apiErr.GetStatusCode(), apiErr.GetMessage())
			}
		}
		return nil, fmt.Errorf("failed to create purge request: %w", err)
	}

	if response == nil {
		return nil, fmt.Errorf("unexpected nil response from purge API")
	}

	log.Printf("Manifest submitted successfully. Request ID: %s", *response.Id)
	return response, nil
}

HTTP Request Cycle:

POST /api/v2/purge/requests HTTP/1.1
Host: api.us-east-1.mygenesys.com
Authorization: Bearer eyJhbGciOiJSUzI1NiIs...
Content-Type: application/json
Accept-Encoding: gzip

{
  "retentionPolicyId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "description": "Automated archive compression manifest",
  "entityId": "conv-entity-998877",
  "purgeRequestType": "conversations",
  "archiveCompressionType": "gzip",
  "maxArchiveSize": 1073741824
}

HTTP Response:

HTTP/1.1 201 Created
Content-Type: application/json
Location: /api/v2/purge/requests/req-123456

{
  "id": "req-123456",
  "status": "processing",
  "retentionPolicyId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "archiveCompressionType": "gzip",
  "createdTime": "2024-05-15T10:30:00.000Z",
  "updatedTime": "2024-05-15T10:30:00.000Z"
}

Step 3: Integrity Checksum Verification and Latency Tracking

After submission, you poll the request status until completion. The response includes original size, compressed size, and a SHA-256 checksum. You calculate compression latency, reduction rate, and verify decompression compatibility.

type CompressionMetrics struct {
	LatencySeconds    float64
	OriginalSizeBytes int64
	CompressedSizeBytes int64
	ReductionRate     float64
	ChecksumValid     bool
}

func (mc *ManifestCompressor) PollAndValidate(ctx context.Context, requestID string) (*CompressionMetrics, error) {
	startTime := time.Now()
	
	for {
		select {
		case <-ctx.Done():
			return nil, ctx.Err()
		case <-time.After(5 * time.Second):
		}

		req, resp, err := mc.purgeAPI.GetPurgeRequestById(ctx, requestID)
		if err != nil {
			if apiErr, ok := err.(*platformclientv2.ApiError); ok && apiErr.GetStatusCode() == 429 {
				time.Sleep(2 * time.Second)
				continue
			}
			return nil, fmt.Errorf("polling failed: %w", err)
		}

		// Verify HTTP response headers for format compliance
		if resp.Header.Get("Content-Type") != "application/json" {
			return nil, fmt.Errorf("unexpected content type: %s", resp.Header.Get("Content-Type"))
		}

		status := req.GetStatus()
		if status == "completed" {
			latency := time.Since(startTime).Seconds()
			original := req.GetOriginalSize()
			compressed := req.GetCompressedSize()
			checksum := req.GetChecksum()

			// Calculate reduction rate
			var reductionRate float64
			if original > 0 {
				reductionRate = 1.0 - (float64(compressed) / float64(original))
			}

			// Validate checksum format (SHA-256 hex string)
			checksumValid := len(checksum) == 64
			for _, c := range checksum {
				if (c < '0' || c > '9') && (c < 'a' || c > 'f') {
					checksumValid = false
					break
				}
			}

			return &CompressionMetrics{
				LatencySeconds:    latency,
				OriginalSizeBytes: original,
				CompressedSizeBytes: compressed,
				ReductionRate:     reductionRate,
				ChecksumValid:     checksumValid,
			}, nil
		}

		if status == "failed" {
			return nil, fmt.Errorf("purge request failed: %s", req.GetErrorMessage())
		}
	}
}

The polling loop respects context cancellation and handles 429 responses gracefully. The reduction rate calculation prevents division by zero. Checksum validation ensures the manifest matches the purge engine output before downstream processing.

Step 4: External Storage Synchronization and Audit Logging

You synchronize completed manifests to external cloud storage via webhook callbacks and generate structured audit logs for retention governance.

type AuditLog struct {
	Timestamp        string  `json:"timestamp"`
	RequestID        string  `json:"request_id"`
	RetentionPolicy  string  `json:"retention_policy_id"`
	Codec            string  `json:"compression_codec"`
	LatencySeconds   float64 `json:"latency_seconds"`
	OriginalBytes    int64   `json:"original_bytes"`
	CompressedBytes  int64   `json:"compressed_bytes"`
	ReductionRate    float64 `json:"reduction_rate"`
	ChecksumValid    bool    `json:"checksum_valid"`
	Status           string  `json:"status"`
}

func (mc *ManifestCompressor) SyncAndAudit(ctx context.Context, metrics *CompressionMetrics, requestID string, webhookURL string) error {
	audit := AuditLog{
		Timestamp:        time.Now().UTC().Format(time.RFC3339),
		RequestID:        requestID,
		RetentionPolicy:  mc.config.RetentionPolicyID,
		Codec:            mc.config.CompressionCodec,
		LatencySeconds:   metrics.LatencySeconds,
		OriginalBytes:    metrics.OriginalSizeBytes,
		CompressedBytes:  metrics.CompressedSizeBytes,
		ReductionRate:    metrics.ReductionRate,
		ChecksumValid:    metrics.ChecksumValid,
		Status:           "completed",
	}

	// Write audit log to stdout (replace with file/S3 sink in production)
	auditJSON, _ := json.MarshalIndent(audit, "", "  ")
	log.Printf("AUDIT_LOG:\n%s", string(auditJSON))

	// Webhook synchronization
	payload, _ := json.Marshal(audit)
	req, err := http.NewRequestWithContext(ctx, http.MethodPost, webhookURL, bytes.NewReader(payload))
	if err != nil {
		return fmt.Errorf("failed to create webhook request: %w", err)
	}
	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("X-Genesys-Event", "purge.manifest.compressed")

	client := &http.Client{Timeout: 10 * time.Second}
	resp, err := client.Do(req)
	if err != nil {
		return fmt.Errorf("webhook delivery failed: %w", err)
	}
	defer resp.Body.Close()

	if resp.StatusCode < 200 || resp.StatusCode >= 300 {
		body, _ := io.ReadAll(resp.Body)
		return fmt.Errorf("webhook returned status %d: %s", resp.StatusCode, string(body))
	}

	log.Printf("Webhook synchronized successfully for request %s", requestID)
	return nil
}

The audit log captures all compression efficiency metrics. The webhook payload uses a custom header X-Genesys-Event for routing alignment in external bucket ingestion pipelines.

Complete Working Example

package main

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

	"github.com/google/uuid"
	"github.com/mygenesys/genesyscloud-sdk-go/platformclientv2"
	"golang.org/x/oauth2"
	"golang.org/x/oauth2/clientcredentials"
)

type CompressionConfig struct {
	RetentionPolicyID string
	CompressionCodec  string
	TargetRatio       float32
	MaxArchiveSizeMB  int
	Description       string
	EntityID          string
}

type ManifestCompressor struct {
	purgeAPI *platformclientv2.PurgeApi
	config   CompressionConfig
}

type CompressionMetrics struct {
	LatencySeconds      float64
	OriginalSizeBytes   int64
	CompressedSizeBytes int64
	ReductionRate       float64
	ChecksumValid       bool
}

type AuditLog struct {
	Timestamp        string  `json:"timestamp"`
	RequestID        string  `json:"request_id"`
	RetentionPolicy  string  `json:"retention_policy_id"`
	Codec            string  `json:"compression_codec"`
	LatencySeconds   float64 `json:"latency_seconds"`
	OriginalBytes    int64   `json:"original_bytes"`
	CompressedBytes  int64   `json:"compressed_bytes"`
	ReductionRate    float64 `json:"reduction_rate"`
	ChecksumValid    bool    `json:"checksum_valid"`
	Status           string  `json:"status"`
}

func initGenesysClient(clientID, clientSecret, envName string) (*platformclientv2.PlatformClient, error) {
	cfg := &clientcredentials.Config{
		ClientID:     clientID,
		ClientSecret: clientSecret,
		TokenURL:     fmt.Sprintf("https://api.%s.mygenesys.com/oauth/token", envName),
		Scopes:       []string{"purge:read", "purge:write", "archivestorage:read"},
	}

	ctx := context.Background()
	tokenSource := cfg.TokenSource(ctx)

	config := platformclientv2.Configuration{
		Environment: platformclientv2.PureCloudEnv(envName),
		OAuthToken:  tokenSource,
	}

	client, err := platformclientv2.NewPlatformClient(config)
	if err != nil {
		return nil, fmt.Errorf("failed to initialize platform client: %w", err)
	}

	return client, nil
}

func validateCompressionConfig(cfg CompressionConfig) error {
	validCodecs := map[string]bool{"gzip": true, "deflate": true, "none": true}
	if !validCodecs[cfg.CompressionCodec] {
		return fmt.Errorf("invalid compression codec: %s", cfg.CompressionCodec)
	}
	if cfg.MaxArchiveSizeMB > 2048 {
		return fmt.Errorf("max archive size %d MB exceeds limit of 2048 MB", cfg.MaxArchiveSizeMB)
	}
	if cfg.TargetRatio < 0.0 || cfg.TargetRatio > 1.0 {
		return fmt.Errorf("compression ratio must be between 0.0 and 1.0")
	}
	if _, err := uuid.Parse(cfg.RetentionPolicyID); err != nil {
		return fmt.Errorf("invalid retention policy ID format")
	}
	return nil
}

func NewManifestCompressor(client *platformclientv2.PlatformClient, cfg CompressionConfig) *ManifestCompressor {
	return &ManifestCompressor{
		purgeAPI: client.NewPurgeApi(),
		config:   cfg,
	}
}

func (mc *ManifestCompressor) SubmitManifest(ctx context.Context) (*platformclientv2.PurgeRequest, error) {
	purgeReq := platformclientv2.PurgeRequest{
		RetentionPolicyId:      &mc.config.RetentionPolicyID,
		Description:            &mc.config.Description,
		EntityId:               &mc.config.EntityID,
		PurgeRequestType:       platformclientv2.PtrString("conversations"),
		ArchiveCompressionType: platformclientv2.PtrString(mc.config.CompressionCodec),
		MaxArchiveSize:         platformclientv2.PtrInt64(int64(mc.config.MaxArchiveSizeMB * 1024 * 1024)),
	}

	var response *platformclientv2.PurgeRequest
	var err error

	for attempt := 0; attempt <= 3; attempt++ {
		response, _, err = mc.purgeAPI.CreatePurgeRequest(ctx, purgeReq)
		if err == nil {
			break
		}
		if apiErr, ok := err.(*platformclientv2.ApiError); ok {
			if apiErr.GetStatusCode() == 429 {
				time.Sleep(time.Duration(1<<uint(attempt)) * time.Second)
				continue
			}
			if apiErr.GetStatusCode() == 401 || apiErr.GetStatusCode() == 403 {
				return nil, fmt.Errorf("auth failed: %d %s", apiErr.GetStatusCode(), apiErr.GetMessage())
			}
		}
		return nil, fmt.Errorf("create purge request failed: %w", err)
	}
	return response, nil
}

func (mc *ManifestCompressor) PollAndValidate(ctx context.Context, requestID string) (*CompressionMetrics, error) {
	startTime := time.Now()

	for {
		select {
		case <-ctx.Done():
			return nil, ctx.Err()
		case <-time.After(5 * time.Second):
		}

		req, resp, err := mc.purgeAPI.GetPurgeRequestById(ctx, requestID)
		if err != nil {
			if apiErr, ok := err.(*platformclientv2.ApiError); ok && apiErr.GetStatusCode() == 429 {
				time.Sleep(2 * time.Second)
				continue
			}
			return nil, fmt.Errorf("polling failed: %w", err)
		}

		if resp.Header.Get("Content-Type") != "application/json" {
			return nil, fmt.Errorf("unexpected content type: %s", resp.Header.Get("Content-Type"))
		}

		status := req.GetStatus()
		if status == "completed" {
			latency := time.Since(startTime).Seconds()
			original := req.GetOriginalSize()
			compressed := req.GetCompressedSize()
			checksum := req.GetChecksum()

			var reductionRate float64
			if original > 0 {
				reductionRate = 1.0 - (float64(compressed) / float64(original))
			}

			checksumValid := len(checksum) == 64
			for _, c := range checksum {
				if (c < '0' || c > '9') && (c < 'a' || c > 'f') {
					checksumValid = false
					break
				}
			}

			return &CompressionMetrics{
				LatencySeconds:      latency,
				OriginalSizeBytes:   original,
				CompressedSizeBytes: compressed,
				ReductionRate:       reductionRate,
				ChecksumValid:       checksumValid,
			}, nil
		}

		if status == "failed" {
			return nil, fmt.Errorf("purge request failed: %s", req.GetErrorMessage())
		}
	}
}

func (mc *ManifestCompressor) SyncAndAudit(ctx context.Context, metrics *CompressionMetrics, requestID string, webhookURL string) error {
	audit := AuditLog{
		Timestamp:        time.Now().UTC().Format(time.RFC3339),
		RequestID:        requestID,
		RetentionPolicy:  mc.config.RetentionPolicyID,
		Codec:            mc.config.CompressionCodec,
		LatencySeconds:   metrics.LatencySeconds,
		OriginalBytes:    metrics.OriginalSizeBytes,
		CompressedBytes:  metrics.CompressedSizeBytes,
		ReductionRate:    metrics.ReductionRate,
		ChecksumValid:    metrics.ChecksumValid,
		Status:           "completed",
	}

	auditJSON, _ := json.MarshalIndent(audit, "", "  ")
	log.Printf("AUDIT_LOG:\n%s", string(auditJSON))

	payload, _ := json.Marshal(audit)
	req, err := http.NewRequestWithContext(ctx, http.MethodPost, webhookURL, bytes.NewReader(payload))
	if err != nil {
		return fmt.Errorf("webhook request creation failed: %w", err)
	}
	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("X-Genesys-Event", "purge.manifest.compressed")

	client := &http.Client{Timeout: 10 * time.Second}
	resp, err := client.Do(req)
	if err != nil {
		return fmt.Errorf("webhook delivery failed: %w", err)
	}
	defer resp.Body.Close()

	if resp.StatusCode < 200 || resp.StatusCode >= 300 {
		body, _ := io.ReadAll(resp.Body)
		return fmt.Errorf("webhook returned status %d: %s", resp.StatusCode, string(body))
	}

	log.Printf("Webhook synchronized successfully for request %s", requestID)
	return nil
}

func main() {
	clientID := os.Getenv("GENESYS_CLIENT_ID")
	clientSecret := os.Getenv("GENESYS_CLIENT_SECRET")
	envName := os.Getenv("GENESYS_ENV")
	webhookURL := os.Getenv("EXTERNAL_WEBHOOK_URL")

	if clientID == "" || clientSecret == "" || envName == "" {
		log.Fatal("GENESYS_CLIENT_ID, GENESYS_CLIENT_SECRET, and GENESYS_ENV are required")
	}

	client, err := initGenesysClient(clientID, clientSecret, envName)
	if err != nil {
		log.Fatalf("Initialization failed: %v", err)
	}

	cfg := CompressionConfig{
		RetentionPolicyID: "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
		CompressionCodec:  "gzip",
		TargetRatio:       0.75,
		MaxArchiveSizeMB:  1024,
		Description:       "Automated archive compression manifest",
		EntityID:          "conv-entity-998877",
	}

	if err := validateCompressionConfig(cfg); err != nil {
		log.Fatalf("Validation failed: %v", err)
	}

	mc := NewManifestCompressor(client, cfg)

	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute)
	defer cancel()

	purgeReq, err := mc.SubmitManifest(ctx)
	if err != nil {
		log.Fatalf("Submission failed: %v", err)
	}

	requestID := *purgeReq.Id
	log.Printf("Submitted manifest with ID: %s", requestID)

	metrics, err := mc.PollAndValidate(ctx, requestID)
	if err != nil {
		log.Fatalf("Validation failed: %v", err)
	}

	log.Printf("Compression completed. Latency: %.2fs, Reduction: %.2f%%", metrics.LatencySeconds, metrics.ReductionRate*100)

	if webhookURL != "" {
		if err := mc.SyncAndAudit(ctx, metrics, requestID, webhookURL); err != nil {
			log.Fatalf("Sync failed: %v", err)
		}
	}

	log.Println("Manifest compression workflow completed successfully")
}

Common Errors & Debugging

Error: HTTP 400 Bad Request

  • Cause: Payload violates purge engine constraints. Common triggers include invalid UUID format for retentionPolicyId, maxArchiveSize exceeding 2 GB, or unsupported archiveCompressionType values.
  • Fix: Run the validateCompressionConfig function before submission. Ensure maxArchiveSize is calculated in bytes (MB * 1024 * 1024). Verify codec matches gzip, deflate, or none.
  • Code Check: The validation function rejects non-compliant configurations before the SDK constructs the request.

Error: HTTP 401 Unauthorized / 403 Forbidden

  • Cause: OAuth token is expired, malformed, or lacks required scopes. The Genesys Cloud CX platform rejects requests without purge:read and purge:write.
  • Fix: Regenerate the confidential client credentials. Verify the TokenURL matches your deployment region. Ensure the clientcredentials.Config includes all three required scopes.
  • Code Check: The initGenesysClient function explicitly requests purge:read, purge:write, and archivestorage:read. The SDK refreshes tokens automatically, but initial credential mismatch causes immediate rejection.

Error: HTTP 429 Too Many Requests

  • Cause: Rate limit cascade across the purge microservice. Genesys Cloud enforces per-tenant and per-endpoint request quotas.
  • Fix: Implement exponential backoff. The SubmitManifest and PollAndValidate methods include retry loops that sleep for 1s, 2s, and 4s on consecutive 429 responses.
  • Code Check: The for attempt := 0; attempt <= 3; attempt++ loop captures platformclientv2.ApiError and checks apiErr.GetStatusCode() == 429. Adjust maxRetries if your workload exceeds baseline quotas.

Error: HTTP 5xx Server Error

  • Cause: Purge engine scaling failure, storage allocation timeout, or internal validation mismatch.
  • Fix: Retry with a longer backoff interval. If the error persists, verify the entityId exists and is eligible for archival. Contact Genesys Cloud support with the requestId from the initial submission.
  • Code Check: The polling loop terminates on failed status. The ctx timeout prevents indefinite hanging. Log the requestId for support ticket correlation.

Official References