Build a Kinetic Database in Salesforce Marketing Cloud with Data Extensions and CloudPages

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:

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

  1. A Gmail subscriber opens the email and sees the fallback: the question and three answer buttons.
  2. They tap Yes. The browser opens the CloudPage with the encrypted parameters.
  3. The page writes (or updates) their row: survey-q1 / yes, source fallback.
  4. They see "Thanks — your answer is recorded".
  5. Three seconds later they land on the page Kinetic_Destinations holds 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:

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:

%%[
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 ]%%

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

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.