Build a Kinetic Database in Salesforce Marketing Cloud with Data Extensions and CloudPages
By Sean · September 30, 2026 · 10 min read
Every tab a subscriber opens, every carousel slide they swipe to and every survey answer they tap is a signal. Kinetic's own tracking database can capture those signals for you. But if you send from Salesforce Marketing Cloud, you may want them somewhere else: right next to the subscriber, in a data extension you can segment on and branch a journey from.
This guide builds that in SFMC with two things you already have: a data extension to hold the data, and one CloudPage that does two jobs.
One CloudPage, two jobs
The same CloudPage works as both a back end and a front end.
The back end: kinetic actions. In a kinetic email, every interaction can record itself. When a subscriber taps a tab or swipes to a slide, a CSS rule loads a tiny background image, and that image's URL is your CloudPage. The page writes a row to the data extension and returns nothing anyone sees. This is exactly how Kinetic's own tracking pixels work; you are simply pointing them at your own endpoint.
The front end: the fallback. Inboxes that can't run the interaction (Gmail, Outlook) get the static fallback version. There, a survey question becomes a set of plain links. Tapping an answer opens the same CloudPage in the browser. It records the answer, shows "Thanks, your answer is recorded", and after a few seconds sends the subscriber on to the brand's page.
One page, one data extension, both audiences captured.
Step 1: the data extension
Create a data extension named Kinetic_Actions:
| Field | Type | Primary key | Notes |
|---|---|---|---|
| SubscriberKey | Text (254) | Yes | Who |
| EmailName | Text (100) | Yes | Which email |
| ActionID | Text (50) | Yes | The interaction: carousel, survey-q1, tabs |
| Label | Text (50) | Yes | The item: slide-2, yes, pricing |
| FirstTapped | Date | No | Set once, on the first record |
| LastTapped | Date | No | Updated every time |
| OpenCount | Number | No | Opens in which this action was used |
| Source | Text (20) | No | kinetic or fallback |
| JobID | Text (50) | No | The send it came from |
Why a composite primary key
SubscriberKey + EmailName + ActionID + Label is required on every record anyway, and together they make each record unique: one row per subscriber, per email, per interaction. Repeats update the row instead of piling up duplicates, and "who viewed slide 2 of the spring email" becomes a single lookup.
Two decisions to make up front:
- A survey question where only the latest answer matters. If a subscriber taps "Yes" and later "No", the composite key keeps both rows. When you only want their current answer, drop Label from the key and store the answer as a regular field, so it is updated rather than duplicated.
- Emails you send more than once under the same name (a weekly newsletter, a resend). Every send of that email name lands in the same row. If per-send reporting matters, add JobID to the key.
Timestamps stay out of the key. A timestamp makes every row unique, which defeats the point of the key. FirstTapped and LastTapped carry the timing instead.
What a kinetic action can (and can't) tell you
An email client loads each image once per open. A kinetic action records through an image, so each action can record at most once per open. Tap slide 2 five times and it records once.
When the subscriber leaves the email and comes back, the next open starts fresh, and every action can record again.
So for a three-slide carousel, the most one open can tell you is which slides were visited, not how many times. That is why the count field is OpenCount: the number of separate opens in which the subscriber used that action. It measures genuine return engagement, not repeated tapping.
Step 2: the CloudPage
Create a CloudPage (a landing page) and give it this AMPscript. It reads the parameters, records the action, and then either stops quietly (pixel mode) or shows a confirmation and redirects (page mode).
%%[
SET @sk = RequestParameter("u") /* same names as Kinetic's own pixels */
SET @email = RequestParameter("e")
SET @action = RequestParameter("a")
SET @label = RequestParameter("l")
SET @src = RequestParameter("src")
SET @job = RequestParameter("s")
SET @mode = RequestParameter("m") /* empty = pixel */
SET @now = Now()
/* An action with no separate label stores the action as its label */
IF EMPTY(@label) THEN SET @label = @action ENDIF
IF NOT EMPTY(@sk) AND NOT EMPTY(@email) AND NOT EMPTY(@action) THEN
SET @rows = LookupRows("Kinetic_Actions",
"SubscriberKey", @sk, "EmailName", @email, "ActionID", @action, "Label", @label)
IF RowCount(@rows) > 0 THEN
SET @count = Add(Field(Row(@rows, 1), "OpenCount"), 1)
UpsertData("Kinetic_Actions", 4,
"SubscriberKey", @sk, "EmailName", @email, "ActionID", @action, "Label", @label,
"LastTapped", @now, "OpenCount", @count, "Source", @src, "JobID", @job)
ELSE
InsertData("Kinetic_Actions",
"SubscriberKey", @sk, "EmailName", @email, "ActionID", @action, "Label", @label,
"FirstTapped", @now, "LastTapped", @now, "OpenCount", 1, "Source", @src, "JobID", @job)
ENDIF
ENDIF
]%%
The lookup-then-write split is what keeps FirstTapped fixed: the first record is inserted with both timestamps, and every later one only moves LastTapped and adds to OpenCount. UpsertData is also what keeps a repeat from failing, since a plain insert against an existing composite key is a primary key violation.
Page mode: confirm, then redirect
Below the AMPscript, the page decides what to show. In pixel mode it shows nothing. In page mode it thanks the subscriber and forwards them:
%%[
IF @mode == "page" THEN
/* Look up the destination for this answer — never take it from the URL */
SET @dest = Lookup("Kinetic_Destinations", "URL", "ActionID", @action, "Label", @label)
IF EMPTY(@dest) THEN SET @dest = "https://www.yourbrand.com" ENDIF
]%%
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta http-equiv="refresh" content="3;url=%%=v(@dest)=%%">
<title>Thanks!</title>
</head>
<body style="font-family: Arial, sans-serif; text-align: center; padding: 64px 24px;">
<h1>Thanks — your answer is recorded.</h1>
<p>Taking you to <a href="%%=v(@dest)=%%">the next page</a> in a few seconds…</p>
</body>
</html>
%%[ ENDIF ]%%
Kinetic_Destinations is a small data extension with ActionID, Label and URL: one row per answer, pointing to where that answer should land. Looking the destination up, instead of passing it in the link, matters. A page that redirects to whatever URL it is handed is an open redirect, and it will eventually be used to send people somewhere you didn't intend.
Step 3: wire up the email
The kinetic actions
Each interactive state loads a background image when it becomes active. Pointed at your CloudPage, a carousel slide looks like this:
#slide2:checked ~ * .track-slide2 {
display: block !important;
max-height: 1px !important;
overflow: hidden !important;
background-image: url('https://cloud.yourbrand.com/kinetic-log?u=%%_subscriberkey%%&s=%%jobid%%&e=spring-sale&a=carousel&l=slide-2&src=kinetic');
}
If you build the email in KineticAI, the tracking pixels are already in place. Use Export for ESP with the Salesforce Marketing Cloud preset: it stamps the email name and project, and swaps the subscriber and send placeholders for %%SubscriberKey%% and %%JobID%%. Then replace Kinetic's pixel address (https://kinetic.email/api/track-pixel) with your CloudPage URL. That is the only change: the page reads the same parameter names Kinetic's pixels use (u, s, e, a, src) and ignores the rest. KineticAI writes the action and item together (a=carousel-1), so the page stores that as both ActionID and Label unless you split it with an l parameter.
Test this first: send yourself a test and confirm the personalization strings resolved inside the <style> block. If your setup doesn't resolve them there, move the tracking rules or build the URLs another way.
The fallback links
In the fallback version, build each answer's link with CloudPagesURL. It encrypts the parameters into the link, so nobody can edit them in the address bar, and it carries the subscriber's context to the page:
<a href="%%=RedirectTo(CloudPagesURL(1234, 'u', _subscriberkey, 's', jobid, 'e', 'spring-sale', 'a', 'survey-q1', 'l', 'yes', 'src', 'fallback', 'm', 'page'))=%%">
Yes
</a>
Replace 1234 with your CloudPage's ID. RequestParameter on the page reads these values just as it reads plain ones.
The survey fallback, end to end
- A Gmail subscriber opens the email and sees the fallback: the question and three answer buttons.
- They tap Yes. The browser opens the CloudPage with the encrypted parameters.
- The page writes (or updates) their row:
survey-q1/yes, sourcefallback. - They see "Thanks — your answer is recorded".
- Three seconds later they land on the page
Kinetic_Destinationsholds for that answer.
An Apple Mail subscriber answering the same question inside the email records through the pixel instead: same data extension, source kinetic, no page load at all.
What the data looks like
Here is Kinetic_Actions after a skincare brand sends a two-question survey email, fall-skin-survey. Question 1 asks for the reader's main skin concern; question 2 asks how soon they need a new routine. Every row has EmailName fall-skin-survey, so that column is left out here. (Example data.)
| SubscriberKey | ActionID | Label | FirstTapped | LastTapped | OpenCount | Source |
|---|---|---|---|---|---|---|
| SK-10421 | survey-q1 | dryness | 9/24 9:12 | 9/24 9:12 | 1 | kinetic |
| SK-10421 | survey-q2 | this-week | 9/24 9:13 | 9/26 18:40 | 2 | kinetic |
| SK-10588 | survey-q1 | breakouts | 9/24 11:02 | 9/24 11:02 | 1 | fallback |
| SK-10588 | survey-q2 | just-browsing | 9/24 11:03 | 9/24 11:03 | 1 | fallback |
| SK-10733 | survey-q1 | fine-lines | 9/25 7:45 | 9/25 7:45 | 1 | kinetic |
| SK-10733 | survey-q2 | this-month | 9/25 7:46 | 9/25 7:46 | 1 | kinetic |
Six rows already say a lot:
- SK-10421 has dry skin and needs a routine this week — and when they reopened the email two days later, they tapped that answer again (
OpenCount2). That is a ready buyer. - SK-10588 answered from Gmail, through the fallback page, and is just browsing. Worth nurturing, not discounting.
- SK-10733 is focused on fine lines and plans to buy within the month.
None of this came from a purchase or a guess. The subscribers told you, inside the email, in two taps.
Putting survey data to work
This is zero-party data: preferences customers give you directly. Some ways a brand can use it:
- Route the next email by answer. A decision split on
survey-q1sends dryness to a hydrating-serum email, breakouts to a clarifying routine, fine-lines to retinol. - Match the offer to the timing. This-week gets an offer within a day. Just-browsing gets education and reviews, with no discount given away.
- Personalize future emails. Any later email can look up the answer and swap a content block to match:
%%[
SET @concern = Lookup("Kinetic_Actions", "Label",
"SubscriberKey", _subscriberkey, "ActionID", "survey-q1")
]%%
%%[ IF @concern == "dryness" THEN ]%%
<!-- hydration hero -->
%%[ ELSEIF @concern == "breakouts" THEN ]%%
<!-- clarifying hero -->
%%[ ELSE ]%%
<!-- default hero -->
%%[ ENDIF ]%%
- Stop sending what doesn't fit. Exclude the dryness segment from acne-product promotions. Fewer irrelevant emails means fewer unsubscribes.
- Grow the profile. Save the answers as contact attributes, or sync them to your CRM, so every channel knows the customer's concern, not only email.
- See where interactivity pays. Compare how many answers came from
kineticversusfallbackto see how much the in-email version adds over a link-out.
Reporting and journeys
A SQL Query Activity turns the table into a summary:
SELECT
EmailName,
ActionID,
Label,
COUNT(*) AS Subscribers,
SUM(OpenCount) AS Engagements
FROM Kinetic_Actions
GROUP BY EmailName, ActionID, Label
To branch a journey on it ("answered Yes", "viewed the pricing tab"), link Kinetic_Actions to your contacts in Contact Builder on SubscriberKey, then use it in a decision split.
Optional: an event log for the full timeline
The main table answers who did what and when they first and last did it. If you also want every record with its time, add a second data extension, Kinetic_Action_Log, with no primary key, and have the page also run a plain InsertData into it on every hit.
Because each action records once per open, the log shows the order in which a subscriber first reached each slide or tab in each open, and when they came back. It's the table for funnel and timing questions; keep segmenting on the main one.
Before you go live
- Page views. Every CloudPage load, including each pixel, may count against your account's page-view allowance (super messages). Check your contract before tracking a large send.
- Test with a real send. Confirm the pixel URLs resolve in
<style>, that a tap writes a row, that a repeat updates it, and that the fallback page redirects. - Keep parameter values short and plain. They form the primary key, and they're easier to report on.
The result: every interaction your kinetic email captures lands in Marketing Cloud, ready to segment on and branch a journey from, whether the subscriber tapped inside the email or in the browser.