When users submit a form and are redirected to a thank-you page, a fundamental challenge emerges: the form submission data vanishes. This happens because HTTP redirects are stateless by design. The browser makes a fresh request to the new page, and the original POST data is lost in transit.
Yet modern business scenarios demand immediate access to this data. Analytics platforms need conversion details to track campaign effectiveness 📊. Live chat widgets require user context to provide personalized support 💬. Scheduling tools need pre-filled information to eliminate friction 📅.
This article presents a production-ready solution for persisting form submission data across redirects in Xperience by Kentico (XbyK), enabling seamless integrations with JavaScript-based third-party tools. We'll explore why XbyK forms are the right choice, examine real-world integration scenarios, and provide a comprehensive implementation guide.

Why Choose Xperience by Kentico Forms?
Building custom forms from scratch may seem appealing, but it comes with hidden costs: development time, maintenance burden, security vulnerabilities, and feature gaps. XbyK forms eliminate these challenges by providing enterprise-grade capabilities out of the box.
Built-In Features That Accelerate Time-to-Market
🧱 Form Builder: Visual drag-and-drop designer that empowers content editors to create and modify forms without developer intervention, reducing bottlenecks and enabling rapid iteration.
🔌 Form Widget: Seamless Page Builder integration for effortless form placement across your site, maintaining design consistency while giving editors flexibility.
📄 Form Submissions View: Built-in administration interface to view, search, and manage submissions. You can order submitted data by column headers, search for specific submissions, and view detailed information for each submission.
📨 Autoresponder & Notification Emails: Automated email responses to users and instant admin notifications, ensuring timely follow-up and improved user experience.
🔗 Contact Mapping: Automatic contact creation and updates with marketing automation integration, enabling sophisticated lead nurturing workflows without manual data entry.
🛠️ Form APIs: Comprehensive programmatic interfaces for custom operations, extensibility, and integration with external systems.
✔️ Validation Rules: Robust validation that ensures data quality while providing immediate user feedback.
👁️🗨️ Visibility Conditions: Allows you to restrict the display of select form fields only to users that meet specific criteria.
🤖 Automation Processes: Email workflow automation triggered by form submissions, enabling sophisticated multi-step campaigns based on user behavior.
The Business Case
These features deliver measurable business value:
⌚ Reduced Development Time: What might take weeks to build custom is available immediately
💰 Lower Total Cost of Ownership: No maintenance burden for form infrastructure
✨ Better User Experience: Proven, polished form interactions that increase conversion rates
🛡️ Enhanced Security: Enterprise-grade security maintained by the platform vendor
📈 Scalability: Handle thousands of submissions without performance concerns
The pattern we'll explore works seamlessly with XbyK forms, leveraging their built-in submission handling while adding the capability to pass data to JavaScript-based tools after redirect.
Real-World Integration Scenarios
Passing form submission data to JavaScript-based tools after redirect isn't just a technical capability. It's a business enabler that directly impacts conversion rates, customer satisfaction, and operational efficiency.
The User Experience Impact
❌ Without this capability:
- User fills out a form with their information
- User submits and is redirected to a thank-you page
- User encounters a chat widget or scheduling tool
- User must re-enter the same information they just provided
- User experiences friction and may abandon the process
✅ With form data persistence:
- User fills out a form once
- User submits and is redirected
- Third-party tools are pre-populated with their information
- User experiences a seamless, personalized interaction
- Conversion rates improve, customer satisfaction increases ✨
Common Integration Use Cases
📉 Analytics Platforms (Google Analytics, Mixpanel)
- Track detailed conversion data with form field values
- Attribute conversions to specific campaigns and channels
- Impact: Better marketing ROI and data-driven decision making
💬 Live Chat Widgets (Intercom, Drift)
- Pre-populate user information for contextual support
- Enable support teams to see what the user just submitted
- Impact: Improved customer satisfaction and reduced support costs
⏱️ Scheduling Tools (Calendly, Cal.com)
- Pre-fill scheduler with user details from form submission
- Eliminate redundant data entry for meeting bookings
- Impact: Higher meeting booking rates and faster sales cycles
When to Use This Pattern
👉 Use this client-side pattern when:
- Integrating with JavaScript widgets that load on the thank-you page
- Third-party tools require data immediately upon page load
- You need to support multiple different tool integrations
🖐️ Use server-side alternatives when:
- Integrating with backend APIs (CRM systems, databases)
- Data needs to flow directly between servers
- You're working with sensitive data that shouldn't touch the client
Technical Overview: The Challenge & Solution
The Core Problem
HTTP redirects are stateless by design. When a user submits a form via POST and the server responds with a 302 redirect, the browser makes an entirely new GET request to the redirect URL. The original POST data is intentionally not carried forward.
This design is correct for general web security and privacy, but it creates a specific challenge: JavaScript widgets on the thank-you page have no access to the form data that was just submitted.
Why Common Workarounds Fall Short
URL Parameters - Passing data in query strings has critical flaws:
- 🔒 Security risk: Sensitive data exposed in URLs, browser history, and server logs
- 📏 Length limitations: URLs have size constraints (typically 2,000-8,000 characters)
- 👁️ Privacy concerns: Data visible to users and easily copied/shared
Session Storage - While better than URL parameters, session storage has limitations:
- Complexity: Requires session management infrastructure
- Scalability concerns: Server-side sessions can impact performance at scale
- Race conditions: Timing issues between form submission and redirect
The Solution: Secure Cookie + Cache Pattern
Our pattern combines the strengths of multiple technologies to create a secure, reliable, and performant solution:
⬇️ User submits form
⬇️ Generate unique GUID, store with form record
⬇️ Generate random access key, store in server-side cache
⬇️ Set HTTP cookie with access key
⬇️ Redirect to thank-you page (cookie survives redirect)
⬇️ Thank-you page loads
⬇️ Read access key from cookie
⬇️ Query cache with access key to get submission GUID
⬇️ Remove GUID from cache (one-time use)
⬇️ Query form submission using GUID
⬇️ Pass form data to JavaScript widgets
➡️ Initialize third-party tools with pre-populated data
Why This Pattern Works
🛡️ Security
- Temporary access keys that expire automatically (10-minute default)
- One-time use: GUID is removed from cache after retrieval
- No sensitive data in cookies (only random access keys)
- HttpOnly cookies prevent JavaScript access
- Secure flag ensures HTTPS-only transmission
- SameSite=Lax prevents CSRF attacks
🪨 Reliability
- Cookies survive redirects automatically
- No race conditions or timing issues
- Browser handles cookie persistence across page transitions
⚡ Performance
- In-memory cache access is extremely fast (microseconds)
- No database queries until the thank-you page loads
- Minimal overhead on form submission process
- Automatic cleanup via cache expiration
🪢 Flexibility
- Works with any JavaScript widget
- Can support multiple simultaneous integrations
- Easy to extend for new use cases
Step-by-Step Implementation Guide
This comprehensive implementation guide provides production-ready code you can adapt to your specific requirements.
1️⃣ Create the Form GUID Service Interface
First, define an interface that abstracts the operations for storing and retrieving form submission GUIDs:
using CMS.OnlineForms;
using Microsoft.AspNetCore.Http;
public interface IFormSubmissionGuidService
{
/// <summary>
/// Stores a form submission GUID securely for retrieval after redirect.
/// Returns a temporary access key that can be used to retrieve the GUID.
/// </summary>
string StoreSubmissionGuidForAccess(Guid submissionGuid);
/// <summary>
/// Retrieves and removes the stored submission GUID using the access key.
/// This is a one-time operation - the GUID is removed after retrieval.
/// </summary>
Guid? GetAndClearSubmissionGuid(string accessKey);
/// <summary>
/// Stores the submission GUID and sets the access key in a cookie.
/// </summary>
string StoreSubmissionGuidAndSetCookie(Guid submissionGuid, HttpContext httpContext);
/// <summary>
/// Processes submission GUID generation and storage for a form submission.
/// </summary>
Task StoreSubmissionGuidAsync(BizFormItem formDataItem, HttpContext httpContext);
}
This interface provides a clean contract for the service operations, making it easy to test and maintain.
2️⃣ Implement the Form GUID Service
Implement the service using XbyK's cache system for secure storage:
using CMS.Helpers;
using CMS.OnlineForms;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
public class FormSubmissionGuidService : IFormSubmissionGuidService
{
private readonly ILogger<FormSubmissionGuidService> _logger;
private const string CACHE_KEY_PREFIX = "FormSubmissionGuid";
private const int CACHE_EXPIRATION_MINUTES = 10;
public FormSubmissionGuidService(ILogger<FormSubmissionGuidService> logger)
{
_logger = logger;
}
public string StoreSubmissionGuidForAccess(Guid submissionGuid)
{
try
{
// Generate a random access key (16-character hex string)
var accessKey = Guid.NewGuid().ToString("N")[..16];
var cacheKey = $"{CACHE_KEY_PREFIX}_{accessKey}";
// Store the GUID in cache with expiration
CacheHelper.Add(
cacheKey,
submissionGuid,
CacheHelper.GetCacheDependency(CACHE_KEY_PREFIX),
DateTime.Now.AddMinutes(CACHE_EXPIRATION_MINUTES),
TimeSpan.Zero
);
_logger.LogDebug(
"Stored submission GUID {SubmissionGuid} with access key {AccessKey}",
submissionGuid,
accessKey
);
return accessKey;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error storing submission GUID");
throw;
}
}
public Guid? GetAndClearSubmissionGuid(string accessKey)
{
try
{
if (string.IsNullOrWhiteSpace(accessKey))
{
return null;
}
var cacheKey = $"{CACHE_KEY_PREFIX}_{accessKey}";
var submissionGuid = CacheHelper.GetItem(cacheKey) as Guid?;
if (!submissionGuid.HasValue)
{
_logger.LogDebug("No submission GUID found for access key {AccessKey}", accessKey);
return null;
}
// Remove from cache after retrieval (one-time use)
CacheHelper.Remove(cacheKey);
_logger.LogDebug(
"Retrieved and cleared submission GUID {SubmissionGuid}",
submissionGuid.Value
);
return submissionGuid.Value;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error retrieving submission GUID for access key {AccessKey}", accessKey);
return null;
}
}
public string StoreSubmissionGuidAndSetCookie(Guid submissionGuid, HttpContext httpContext)
{
var accessKey = StoreSubmissionGuidForAccess(submissionGuid);
// Set cookie with access key
httpContext.Response.Cookies.Append(
"FormSubmissionAccessKey",
accessKey,
new CookieOptions
{
HttpOnly = true,
Secure = true,
SameSite = SameSiteMode.Lax,
Expires = DateTimeOffset.Now.AddMinutes(CACHE_EXPIRATION_MINUTES)
}
);
return accessKey;
}
public async Task StoreSubmissionGuidAsync(BizFormItem formDataItem, HttpContext httpContext)
{
try
{
// Get the GUID that was set during form processing
var submissionGuid = formDataItem.GetGuidValue("SubmissionGUID", Guid.Empty);
if (submissionGuid == Guid.Empty)
{
_logger.LogDebug("No submission GUID found for form {FormName}",
formDataItem.BizFormInfo?.FormName);
return;
}
// Optional: Add validation logic here
// For example, only store GUID for specific forms or under certain conditions
if (ShouldStoreSubmissionGuid(formDataItem))
{
StoreSubmissionGuidAndSetCookie(submissionGuid, httpContext);
_logger.LogDebug(
"Stored submission GUID {SubmissionGuid} for form {FormName}",
submissionGuid,
formDataItem.BizFormInfo?.FormName
);
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Error processing submission GUID");
throw;
}
}
private bool ShouldStoreSubmissionGuid(BizFormItem formDataItem)
{
// Add your business logic here
// For example, only store for specific forms:
// var formName = formDataItem.BizFormInfo?.FormName?.ToLower();
// return formName == "demorequest" || formName == "contactform";
return true; // Default: store for all forms
}
}
This implementation uses XbyK's CacheHelper for storage, generates random access keys, implements one-time use retrieval, and sets secure cookie options (HttpOnly, Secure, SameSite).
3️⃣ Generate GUID Before Form Insert
Hook into XbyK's form event system to generate a GUID before the form is inserted into the database:
using CMS.Core;
using CMS.DataEngine;
using CMS.OnlineForms;
public class FormSubmissionBeforeInsertHandler
: IInfoObjectEventHandler<InfoObjectBeforeInsertEvent<BizFormItem>>
{
private readonly ILogger<FormSubmissionBeforeInsertHandler> _logger;
public FormSubmissionBeforeInsertHandler(ILogger<FormSubmissionBeforeInsertHandler> logger)
{
_logger = logger;
}
public void Handle(InfoObjectBeforeInsertEvent<BizFormItem> infoObjectEvent)
{
try
{
var formDataItem = infoObjectEvent.InfoObject;
var formName = formDataItem.BizFormInfo?.FormName;
// Generate GUID for forms that need post-redirect access
if (ShouldGenerateSubmissionGuid(formName))
{
var submissionGuid = Guid.NewGuid();
formDataItem.SetValue("SubmissionGUID", submissionGuid);
_logger.LogDebug(
"Generated submission GUID {SubmissionGuid} for form {FormName}",
submissionGuid,
formName
);
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Error generating submission GUID");
// Don't throw - allow form submission to proceed
}
}
private bool ShouldGenerateSubmissionGuid(string? formName)
{
// Add your business logic here
// For example, only generate for specific forms:
// var formsToTrack = new[] { "demorequest", "contactform", "newslettersignup" };
// return formsToTrack.Contains(formName?.ToLower());
return true; // Default: generate for all forms
}
}
Register this handler in your Program.cs or Startup.cs:
using CMS.Core;
// In your service configuration
services.AddSingleton<IInfoObjectEventHandler<InfoObjectBeforeInsertEvent<BizFormItem>>,
FormSubmissionBeforeInsertHandler>();
4️⃣ Store GUID After Form Submission
Create an event handler that stores the GUID after the form is successfully submitted:
using CMS.Core;
using CMS.DataEngine;
using CMS.OnlineForms;
using Microsoft.AspNetCore.Http;
public class FormSubmissionAfterInsertHandler
: IInfoObjectEventHandler<InfoObjectAfterInsertEvent<BizFormItem>>
{
private readonly IFormSubmissionGuidService _guidService;
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly ILogger<FormSubmissionAfterInsertHandler> _logger;
public FormSubmissionAfterInsertHandler(
IFormSubmissionGuidService guidService,
IHttpContextAccessor httpContextAccessor,
ILogger<FormSubmissionAfterInsertHandler> logger)
{
_guidService = guidService;
_httpContextAccessor = httpContextAccessor;
_logger = logger;
}
public void Handle(InfoObjectAfterInsertEvent<BizFormItem> infoObjectEvent)
{
try
{
var httpContext = _httpContextAccessor.HttpContext;
if (httpContext == null)
{
_logger.LogWarning("HttpContext is null, cannot store submission GUID");
return;
}
// Store the GUID and set cookie
_guidService.StoreSubmissionGuidAsync(
infoObjectEvent.InfoObject,
httpContext
).Wait(); // XbyK event handlers don't support async, so Wait() is required
_logger.LogDebug("Successfully processed submission GUID after form insert");
}
catch (Exception ex)
{
_logger.LogError(ex, "Error storing submission GUID after form insert");
// Don't throw - form submission already succeeded
}
}
}
Register this handler and the service:
// Register the service
services.AddScoped<IFormSubmissionGuidService, FormSubmissionGuidService>();
// Register the event handler
services.AddSingleton<IInfoObjectEventHandler<InfoObjectAfterInsertEvent<BizFormItem>>,
FormSubmissionAfterInsertHandler>();
// Required for accessing HttpContext in event handlers
services.AddHttpContextAccessor();
5️⃣ Create Widget/View Component to Retrieve Data
Create a view component that retrieves the form submission data on the thank you page:
using CMS.OnlineForms;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ViewComponents;
public class FormSubmissionDataViewComponent : ViewComponent
{
private readonly IFormSubmissionGuidService _guidService;
private readonly ILogger<FormSubmissionDataViewComponent> _logger;
public FormSubmissionDataViewComponent(
IFormSubmissionGuidService guidService,
ILogger<FormSubmissionDataViewComponent> logger)
{
_guidService = guidService;
_logger = logger;
}
public IViewComponentResult Invoke()
{
try
{
// Read access key from cookie
var accessKey = Request.Cookies["FormSubmissionAccessKey"];
if (string.IsNullOrWhiteSpace(accessKey))
{
_logger.LogDebug("No form submission access key found in cookie");
return View(new FormSubmissionDataViewModel());
}
// Retrieve submission GUID from cache
var submissionGuid = _guidService.GetAndClearSubmissionGuid(accessKey);
if (!submissionGuid.HasValue)
{
_logger.LogDebug("No submission GUID found for access key");
return View(new FormSubmissionDataViewModel());
}
// Find the form submission
var formSubmission = FindFormSubmissionByGuid(submissionGuid.Value);
if (formSubmission == null)
{
_logger.LogWarning("Form submission not found for GUID {SubmissionGuid}",
submissionGuid.Value);
return View(new FormSubmissionDataViewModel());
}
// Create view model with form data
var viewModel = new FormSubmissionDataViewModel
{
HasData = true,
Email = formSubmission.GetStringValue("Email", string.Empty),
FirstName = formSubmission.GetStringValue("FirstName", string.Empty),
LastName = formSubmission.GetStringValue("LastName", string.Empty),
Company = formSubmission.GetStringValue("Company", string.Empty),
// Add other fields as needed
};
return View(viewModel);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error retrieving form submission data");
return View(new FormSubmissionDataViewModel());
}
}
private BizFormItem? FindFormSubmissionByGuid(Guid submissionGuid)
{
// Query form submissions to find the one with matching GUID
// Adjust the form class name based on your forms
var formClassName = "bizform.YourFormName"; // Replace with your form class name
return BizFormItemProvider
.GetItems(formClassName)
.WhereEquals("SubmissionGUID", submissionGuid)
.FirstOrDefault();
}
}
public class FormSubmissionDataViewModel
{
public bool HasData { get; set; }
public string Email { get; set; } = string.Empty;
public string FirstName { get; set; } = string.Empty;
public string LastName { get; set; } = string.Empty;
public string Company { get; set; } = string.Empty;
// Add other properties as needed
}
Create a view for the component (Views/Shared/Components/FormSubmissionData/Default.cshtml):
@model FormSubmissionDataViewModel
@if (Model.HasData) {
<div
id="form-submission-data"
data-email="@Model.Email"
data-first-name="@Model.FirstName"
data-last-name="@Model.LastName"
data-company="@Model.Company"
>
<!-- Data is available via data attributes for JavaScript -->
</div>
<script type="application/json" id="form-submission-json">
@Html.Raw(System.Text.Json.JsonSerializer.Serialize(Model))
</script>
}
6️⃣ Initialize JavaScript Widget
Create JavaScript code to read the form data and initialize your widget:
(function () {
"use strict";
// Method 1: Read from data attributes
const dataElement = document.getElementById("form-submission-data");
if (dataElement) {
const formData = {
email: dataElement.dataset.email,
firstName: dataElement.dataset.firstName,
lastName: dataElement.dataset.lastName,
company: dataElement.dataset.company,
};
initializeWidget(formData);
}
// Method 2: Read from JSON script tag
const jsonElement = document.getElementById("form-submission-json");
if (jsonElement) {
try {
const formData = JSON.parse(jsonElement.textContent);
if (formData.hasData) {
initializeWidget(formData);
}
} catch (e) {
console.error("Error parsing form submission data:", e);
}
}
function initializeWidget(formData) {
// Example: Send to analytics
if (window.gtag) {
window.gtag("event", "form_submission", {
email: formData.email,
company: formData.company,
});
}
// Example: Initialize chat widget
if (window.Intercom) {
window.Intercom("boot", {
email: formData.email,
name: `${formData.firstName} ${formData.lastName}`,
company: { name: formData.company },
});
}
console.log("Form data available for widget initialization:", formData);
}
})();
7️⃣ Security & Best Practices
Cookie Security: Configure cookies securely in Program.cs with HttpOnly, Secure, and SameSite settings. Register the cookie in cookie consent if required.
Error Handling: Always handle cases where form data might not be available (direct navigation, expired cookies/cache, missing submissions).
Performance: Set cache expiration to match business requirements (5-10 minutes is typical). Use async/await patterns and query form submissions efficiently.
Testing: Test normal submissions, direct navigation, expired cookies/cache, and edge cases.
This implementation provides a solid foundation that works with any JavaScript widget or service, whether it accepts data via URL parameters, JavaScript API calls, or event tracking. The key is retrieving the form data server-side and making it available to client-side JavaScript in the format required by your specific integration.
🎯 Conclusion
The combination of Xperience by Kentico's enterprise-grade forms and the secure cookie + cache pattern creates a powerful solution for persisting form submission data across redirects. This enables seamless integrations with JavaScript-based third-party tools while maintaining security and performance.
Key Takeaways
✅ XbyK forms provide built-in features that reduce development time and maintenance burden
✅ Business impact: This pattern improves conversion rates, enhances user experience, and enables sophisticated marketing automation
✅ Technical excellence: The implementation leverages XbyK's platform capabilities and follows ASP.NET Core best practices
✅ Flexibility: Works with any JavaScript widget: analytics, chat, scheduling, and more
Next Steps
Adapt to Your Needs: Customize the implementation based on your specific forms and integration requirements.
Monitor Performance: Track cache hit rates, response times, and integration success rates.
Join the Community: This solution was refined through community collaboration. Join the discussion in the Kentico Community Q&A thread 💬.
Further reading
all posts- Front-end & JavaScript
Philips Hue experiments
I created an app for the Anybody hotel that controls Philips Hue lights. However, before I was able to design and implement the app, I researched the possibilities of the technolog…
- Kentico Xperience
Reload Javascript after postback in Kentico
For any kind of form web parts, I like using update panel to avoid full page reload when the form is submitted. In such situation, a postback is invoked. When you execute a Javascr…
Safe attachment URLs resolution in srcset attribute in Page Builder in Kentico Xperience 13
In this post, I will provide you with a quick tip on how to fix the attachment URLs resolution in srcset attribute in Page Builder in Kentico Xperience 13.