diff --git a/prisma/schema.prisma b/prisma/schema.prisma
index 5602163dc..e74a2eaa8 100644
--- a/prisma/schema.prisma
+++ b/prisma/schema.prisma
@@ -505,6 +505,7 @@ model UserCommunicationJourney {
   datetimeCreated    DateTime                     @default(now()) @map("datetime_created")
   userCommunications UserCommunication[]
   campaignName       String?                      @map("campaign_name")
+  variantName        String?                      @map("variant_name")
 
   @@index([userId])
   @@map("user_communication_journey")
diff --git a/src/app/api/public/sms/events/status/route.ts b/src/app/api/public/sms/events/status/route.ts
index 8d3fafc87..d1481c721 100644
--- a/src/app/api/public/sms/events/status/route.ts
+++ b/src/app/api/public/sms/events/status/route.ts
@@ -12,6 +12,7 @@ import { bulkCreateCommunicationJourney } from '@/utils/server/sms/communication
 import { getUserByPhoneNumber, verifySignature } from '@/utils/server/sms/utils'
 import { getLogger } from '@/utils/shared/logger'
 import { toBool } from '@/utils/shared/toBool'
+import { apiUrls } from '@/utils/shared/urls'
 
 enum SMSEventMessageStatus {
   DELIVERED = 'delivered',
@@ -55,13 +56,12 @@ export const POST = withRouteMiddleware(async (request: NextRequest) => {
 
   logger.info('Request URL:', request.url, body)
 
-  const [_, searchParams] = request.url.split('?')
+  const normalizedParams = normalizeSearchParams(request)
 
-  const params = new URLSearchParams(searchParams)
+  const { hasWelcomeMessageInBody, variantName } = normalizedParams
 
-  let journeyType = params.get('journeyType') as UserCommunicationJourneyType | null
-  let campaignName = params.get('campaignName')
-  const hasWelcomeMessageInBody = toBool(params.get('hasWelcomeMessageInBody'))
+  let campaignName = normalizedParams.campaignName
+  let journeyType = normalizedParams.journeyType
 
   const messageId = body.MessageSid
   const messageStatus = body.MessageStatus
@@ -113,7 +113,7 @@ export const POST = withRouteMiddleware(async (request: NextRequest) => {
     }
 
     journeyType = existingMessage.userCommunicationJourney.journeyType
-    campaignName = existingMessage.userCommunicationJourney.campaignName
+    campaignName = existingMessage.userCommunicationJourney.campaignName || ''
   } else {
     if (!journeyType || !campaignName) {
       return new NextResponse('missing search params', {
@@ -132,6 +132,7 @@ export const POST = withRouteMiddleware(async (request: NextRequest) => {
         status: newMessageStatus,
       },
       phoneNumber,
+      variantName,
     })
 
     if (hasWelcomeMessageInBody) {
@@ -168,6 +169,7 @@ export const POST = withRouteMiddleware(async (request: NextRequest) => {
           'Message Id': messageId,
           From: from,
           To: phoneNumber,
+          Variant: variantName,
           'Campaign Name': campaignName,
           'Journey Type': journeyType,
           Error: errorCode,
@@ -180,3 +182,23 @@ export const POST = withRouteMiddleware(async (request: NextRequest) => {
     status: 200,
   })
 })
+
+type SmsStatusCallbackParams = Parameters<typeof apiUrls.smsStatusCallback>[0]
+
+function normalizeSearchParams(request: NextRequest): SmsStatusCallbackParams {
+  const [_, rawSearchParams] = request.url.split('?')
+
+  const searchParams = new URLSearchParams(rawSearchParams)
+
+  const campaignName = searchParams.get('campaignName') || ''
+  const hasWelcomeMessageInBody = toBool(searchParams.get('hasWelcomeMessageInBody'))
+  const journeyType = searchParams.get('journeyType') as UserCommunicationJourneyType
+  const variantName = searchParams.get('variantName') || ''
+
+  return {
+    campaignName,
+    hasWelcomeMessageInBody,
+    journeyType,
+    variantName,
+  }
+}
diff --git a/src/inngest/functions/eventNotification/logic.ts b/src/inngest/functions/eventNotification/logic.ts
index f72d4dcf4..18cfc728a 100644
--- a/src/inngest/functions/eventNotification/logic.ts
+++ b/src/inngest/functions/eventNotification/logic.ts
@@ -115,7 +115,13 @@ async function getNotificationInformationForEvents(
         messages: [
           {
             campaignName: `event-reminder-${event.data.slug}-${event.data.state}-${notificationStrategy}`,
-            smsBody,
+            variants: [
+              {
+                percentage: 100,
+                smsBody,
+              },
+            ],
+            includePendingDoubleOptIn: true,
             userWhereInput: {
               phoneNumber: {
                 in: notifications.map(notification => notification.phoneNumber),
diff --git a/src/inngest/functions/sms/bulkSMSCommunicationJourney.ts b/src/inngest/functions/sms/bulkSMSCommunicationJourney.ts
index 9d19c1f8a..c3b5cb984 100644
--- a/src/inngest/functions/sms/bulkSMSCommunicationJourney.ts
+++ b/src/inngest/functions/sms/bulkSMSCommunicationJourney.ts
@@ -6,7 +6,7 @@ import {
 } from '@prisma/client'
 import { addDays, addHours, differenceInMilliseconds, startOfDay } from 'date-fns'
 import { NonRetriableError } from 'inngest'
-import { chunk, merge, uniq, uniqBy, update } from 'lodash-es'
+import { chunk, merge, shuffle, uniq, uniqBy, update } from 'lodash-es'
 
 import { EnqueueMessagePayload, enqueueSMS } from '@/inngest/functions/sms/enqueueMessages'
 import { countMessagesAndSegments } from '@/inngest/functions/sms/utils/countMessagesAndSegments'
@@ -30,11 +30,15 @@ export interface BulkSmsCommunicationJourneyInngestEventSchema {
 
 export interface BulkSMSPayload {
   messages: Array<{
-    smsBody: string
-    userWhereInput?: GetPhoneNumberOptions['userWhereInput']
-    includePendingDoubleOptIn?: boolean
+    variants: Array<{
+      smsBody: string
+      media?: string[]
+      percentage: number
+      variantName: string
+    }>
     campaignName: string
-    media?: string[]
+    includePendingDoubleOptIn?: boolean
+    userWhereInput?: GetPhoneNumberOptions['userWhereInput']
   }>
   // default to ET: -5
   timezone?: number
@@ -68,9 +72,31 @@ export const bulkSMSCommunicationJourney = inngest.createFunction(
       throw new NonRetriableError('Missing messages to send')
     }
 
-    messages.forEach(({ smsBody, campaignName }, index) => {
-      if (!smsBody) {
-        throw new NonRetriableError(`Missing sms body in message ${index}`)
+    messages.forEach(({ campaignName, variants }, index) => {
+      if (variants.length === 0) {
+        throw new NonRetriableError(`Missing variants in message ${index}`)
+      }
+
+      let fullPercentage = 0
+
+      variants.forEach(({ smsBody, percentage, variantName }, variantIndex) => {
+        if (!smsBody) {
+          throw new NonRetriableError(
+            `Missing sms body in variant ${variantIndex}, message ${index}`,
+          )
+        }
+
+        if (!variantName) {
+          throw new NonRetriableError(`Missing name in variant ${variantIndex}, message ${index}`)
+        }
+
+        fullPercentage += percentage
+      })
+
+      if (Math.ceil(fullPercentage) !== 100) {
+        throw new NonRetriableError(
+          `The total percentages provided (${fullPercentage}%) don't add up to 100%.`,
+        )
       }
 
       if (!campaignName) {
@@ -89,7 +115,7 @@ export const bulkSMSCommunicationJourney = inngest.createFunction(
     const messagesInfo: Record<string, object> = {}
 
     for (const message of messages) {
-      const { campaignName, smsBody, includePendingDoubleOptIn, media, userWhereInput } = message
+      const { campaignName, includePendingDoubleOptIn, userWhereInput, variants } = message
 
       logger.info(prettyStringify(message))
 
@@ -154,7 +180,7 @@ export const bulkSMSCommunicationJourney = inngest.createFunction(
           prettyStringify({
             userHasWelcomeMessage,
             campaignName,
-            smsBody,
+            variants,
           }),
         )
 
@@ -165,6 +191,7 @@ export const bulkSMSCommunicationJourney = inngest.createFunction(
             () =>
               getPhoneNumberList({
                 campaignName,
+                variantNames: variants.map(v => v.variantName),
                 includePendingDoubleOptIn,
                 skip,
                 userWhereInput: customWhere,
@@ -183,27 +210,55 @@ export const bulkSMSCommunicationJourney = inngest.createFunction(
           }
         }
 
-        logger.info('Got phone numbers, adding to messagesPayload')
+        logger.info(`Fetched all phone numbers ${allPhoneNumbers.length}`)
 
-        // Here we're adding the welcome legalese to the bulk text, when doing this we need to register in our DB that the user received the welcome legalese
-        const body = !userHasWelcomeMessage ? addWelcomeMessage(smsBody) : smsBody
+        const phoneNumberVariants = await step.run(
+          `split-phone-numbers-${String(userHasWelcomeMessage)}-${campaignName}`,
+          async () => {
+            // Using uniq outside the while loop, because getPhoneNumberList could return the same phone number in two separate batches
+            const randomizedPhoneNumbers = shuffle(uniq(allPhoneNumbers))
 
-        // Using uniq outside the while loop, because getPhoneNumberList could return the same phone number in two separate batches
+            return splitArrayByPercentages(
+              randomizedPhoneNumbers,
+              variants.map(v => v.percentage), // [60, 40] will return two arrays with 60% and 40% of the phone numbers
+            )
+          },
+        )
+
+        logger.info(`Splitted phone numbers into ${phoneNumberVariants.length} variants`)
+
+        let variantIndex = 0
         // We need to use concat here because using spread is exceeding maximum call stack size
         messagesPayload = messagesPayload.concat(
-          uniq(allPhoneNumbers).map(phoneNumber => ({
-            phoneNumber,
-            messages: [
-              {
-                body,
-                campaignName,
-                journeyType: UserCommunicationJourneyType.BULK_SMS,
-                media,
-                // If the user does not have a welcome message, the body must have it
-                hasWelcomeMessageInBody: !userHasWelcomeMessage,
-              },
-            ],
-          })),
+          variants.reduce<EnqueueMessagePayload[]>((acc, variant) => {
+            const phoneNumbersPortion = phoneNumberVariants[variantIndex]
+
+            logger.info(`Variant ${variantIndex} has ${phoneNumbersPortion.length} phone numbers`)
+
+            variantIndex += 1
+
+            const { smsBody, media, variantName } = variant
+
+            // Here we're adding the welcome legalese to the bulk text, when doing this we need to register in our DB that the user received the welcome legalese
+            const body = !userHasWelcomeMessage ? addWelcomeMessage(smsBody) : smsBody
+
+            return acc.concat(
+              phoneNumbersPortion.map(phoneNumber => ({
+                phoneNumber,
+                messages: [
+                  {
+                    body,
+                    campaignName,
+                    variantName,
+                    journeyType: UserCommunicationJourneyType.BULK_SMS,
+                    media,
+                    // If the user does not have a welcome message, the body must have it
+                    hasWelcomeMessageInBody: !userHasWelcomeMessage,
+                  },
+                ],
+              })),
+            )
+          }, []),
         )
 
         logger.info(`messagesPayload.length ${messagesPayload.length}`)
@@ -356,6 +411,7 @@ export interface GetPhoneNumberOptions {
   skip: number
   userWhereInput?: Prisma.UserGroupByArgs['where']
   campaignName?: string
+  variantNames?: string[]
 }
 
 async function getPhoneNumberList(options: GetPhoneNumberOptions) {
@@ -373,9 +429,15 @@ async function getPhoneNumberList(options: GetPhoneNumberOptions) {
                     campaignName: {
                       not: options.campaignName,
                     },
+                    variantName: {
+                      notIn: options.variantNames,
+                    },
                   },
                   {
                     campaignName: options.campaignName,
+                    variantName: {
+                      in: options.variantNames,
+                    },
                     userCommunications: {
                       every: {
                         status: {
@@ -406,3 +468,24 @@ async function getPhoneNumberList(options: GetPhoneNumberOptions) {
     })
     .then(res => res.map(({ phoneNumber }) => phoneNumber))
 }
+
+function splitArrayByPercentages<T>(array: T[], percentages: number[]) {
+  const totalLength = array.length
+  const dividedArrays: Array<typeof array> = []
+
+  let startIndex = 0
+
+  percentages.forEach((percentage, index) => {
+    let size = Math.floor((percentage / 100) * totalLength)
+
+    // If it's the last percentage, we need to add the remaining elements
+    if (index === percentages.length - 1) {
+      size = totalLength - startIndex
+    }
+
+    dividedArrays.push(array.slice(startIndex, startIndex + size))
+    startIndex += size
+  })
+
+  return dividedArrays
+}
diff --git a/src/inngest/functions/sms/enqueueMessages.ts b/src/inngest/functions/sms/enqueueMessages.ts
index 39492b084..ccbfd7b2f 100644
--- a/src/inngest/functions/sms/enqueueMessages.ts
+++ b/src/inngest/functions/sms/enqueueMessages.ts
@@ -24,6 +24,7 @@ export interface EnqueueMessagePayload {
     body: string
     journeyType: UserCommunicationJourneyType
     campaignName: string
+    variantName: string
     media?: string[]
     hasWelcomeMessageInBody?: boolean
   }>
@@ -150,7 +151,14 @@ export async function enqueueMessages(
 
   const enqueueMessagesPromise = payload.map(async ({ messages, phoneNumber }) => {
     for (const message of messages) {
-      const { body, journeyType, campaignName, media, hasWelcomeMessageInBody } = message
+      const {
+        body,
+        journeyType,
+        campaignName,
+        media,
+        hasWelcomeMessageInBody = false,
+        variantName,
+      } = message
 
       const phoneNumberVariables = variables[phoneNumber] ?? {}
 
@@ -163,6 +171,7 @@ export async function enqueueMessages(
             apiUrls.smsStatusCallback({
               journeyType,
               campaignName,
+              variantName,
               hasWelcomeMessageInBody,
             }),
           ),
diff --git a/src/utils/server/sms/communicationJourney.ts b/src/utils/server/sms/communicationJourney.ts
index 08b99dacd..0b1f1ca08 100644
--- a/src/utils/server/sms/communicationJourney.ts
+++ b/src/utils/server/sms/communicationJourney.ts
@@ -21,6 +21,7 @@ interface BulkCreateCommunicationJourneyPayload {
     status?: CommunicationMessageStatus
   }
   campaignName: string
+  variantName?: string
 }
 
 export async function bulkCreateCommunicationJourney({
@@ -28,6 +29,7 @@ export async function bulkCreateCommunicationJourney({
   journeyType,
   message,
   phoneNumber,
+  variantName,
 }: BulkCreateCommunicationJourneyPayload) {
   const users = await prismaClient.user.findMany({
     where: {
@@ -51,6 +53,7 @@ export async function bulkCreateCommunicationJourney({
         },
         journeyType,
         campaignName,
+        variantName,
       },
       select: {
         userId: true,
@@ -65,6 +68,7 @@ export async function bulkCreateCommunicationJourney({
         journeyType,
         campaignName,
         userId: id,
+        variantName,
       })),
   })
 
@@ -75,6 +79,7 @@ export async function bulkCreateCommunicationJourney({
       },
       journeyType,
       campaignName,
+      variantName,
     },
     select: {
       id: true,
@@ -84,24 +89,22 @@ export async function bulkCreateCommunicationJourney({
 
   if (!message) return
 
-  const createCommunicationPayload = users
-    .map<Prisma.UserCommunicationCreateManyInput>(user => {
-      const communicationJourney = createdCommunicationJourneys.find(
-        ({ userId }) => userId === user.id,
-      )
+  const createCommunicationPayload = users.map<Prisma.UserCommunicationCreateManyInput>(user => {
+    const communicationJourney = createdCommunicationJourneys.find(
+      ({ userId }) => userId === user.id,
+    )
 
-      if (!communicationJourney?.id) {
-        throw new Error(`Couldn't find communicationJourney id for user ${user.id}`)
-      }
+    if (!communicationJourney?.id) {
+      throw new Error(`Couldn't find communicationJourney id for user ${user.id}`)
+    }
 
-      return {
-        messageId: message.id,
-        status: message.status,
-        userCommunicationJourneyId: communicationJourney.id,
-        communicationType: CommunicationType.SMS,
-      }
-    })
-    .filter(communication => !!communication)
+    return {
+      messageId: message.id,
+      status: message.status,
+      userCommunicationJourneyId: communicationJourney.id,
+      communicationType: CommunicationType.SMS,
+    }
+  })
 
   await prismaClient.userCommunication.createMany({
     data: createCommunicationPayload,
diff --git a/src/utils/server/sms/sendSMS.ts b/src/utils/server/sms/sendSMS.ts
index ca4fa3605..3a06766d6 100644
--- a/src/utils/server/sms/sendSMS.ts
+++ b/src/utils/server/sms/sendSMS.ts
@@ -1,6 +1,8 @@
 import { z } from 'zod'
 
 import { isPhoneNumberSupported } from '@/utils/server/sms/utils'
+import { getLogger } from '@/utils/shared/logger'
+import { prettyStringify } from '@/utils/shared/prettyLog'
 import { requiredEnv } from '@/utils/shared/requiredEnv'
 import { NEXT_PUBLIC_ENVIRONMENT } from '@/utils/shared/sharedEnv'
 
@@ -21,6 +23,8 @@ const zodSendSMSSchema = z.object({
 
 export type SendSMSPayload = z.infer<typeof zodSendSMSSchema>
 
+const logger = getLogger('sendSMS')
+
 export const sendSMS = async (payload: SendSMSPayload) => {
   const validatedInput = zodSendSMSSchema.safeParse(payload)
 
@@ -35,6 +39,16 @@ export const sendSMS = async (payload: SendSMSPayload) => {
   }
 
   if (NEXT_PUBLIC_ENVIRONMENT === 'local') {
+    logger.info(
+      'sendSMS localhost',
+      prettyStringify({
+        messagingServiceSid: TWILIO_MESSAGING_SERVICE_SID,
+        body,
+        statusCallback: statusCallbackUrl,
+        to,
+        mediaUrl: media,
+      }),
+    )
     return
   }
 
diff --git a/src/utils/shared/urls/index.ts b/src/utils/shared/urls/index.ts
index 0a3078260..46ae29cb9 100644
--- a/src/utils/shared/urls/index.ts
+++ b/src/utils/shared/urls/index.ts
@@ -1,3 +1,5 @@
+import { UserCommunicationJourneyType } from '@prisma/client'
+
 import { RecentActivityAndLeaderboardTabs } from '@/components/app/pageHome/recentActivityAndLeaderboardTabs'
 import { DEFAULT_LOCALE, SupportedLocale } from '@/intl/locales'
 import { NormalizedDTSIDistrictId } from '@/utils/dtsi/dtsiPersonRoleUtils'
@@ -156,16 +158,15 @@ export const apiUrls = {
     stateCode: string
     district: number
   }) => `/api/public/dtsi/races/usa/${stateCode}/${district}`,
-  smsStatusCallback: ({
-    campaignName,
-    journeyType,
-    hasWelcomeMessageInBody,
-  }: {
+  smsStatusCallback: (params: {
     campaignName: string
-    journeyType: string
-    hasWelcomeMessageInBody?: boolean
+    journeyType: UserCommunicationJourneyType
+    variantName: string
+    hasWelcomeMessageInBody: boolean
   }) =>
-    `/api/public/sms/events/status?campaignName=${campaignName}&journeyType=${journeyType}&hasWelcomeMessageInBody=${String(hasWelcomeMessageInBody ?? false)}`,
+    `/api/public/sms/events/status?${Object.entries(params)
+      .map(([key, value]) => `${key}=${String(value)}`)
+      .join('&')}`,
   decisionDeskPresidentialData: () => '/api/public/decision-desk/usa/presidential',
   decisionDeskStateData: ({ stateCode }: { stateCode: string }) =>
     `/api/public/decision-desk/usa/state/${stateCode}`,