Skip to main content

โœ… Campaign Status Check (A2P)

๐Ÿ“– Overviewโ€‹

The Campaign Status Check job monitors Twilio Application-to-Person (A2P) messaging campaign approval status and automatically sets renewal dates when campaigns transition to verified status. It runs every 6 hours, identifies accounts with campaigns in 'IN_PROGRESS' status, fetches the latest campaign status from Twilio, and sets a renewal date (30 days in the future) when campaigns are verified. This ensures compliance tracking for 10DLC messaging campaigns.

Complete Flow:

  1. Cron Initialization: queue-manager/crons/communication/a2p/campaign.js
  2. Service Processing: queue-manager/services/communication/a2p.js (campaignStatusCheck)
  3. Queue Definition: queue-manager/queues/communication/a2p/campaign.js

Execution Pattern: Periodic polling (every 6 hours) with account-level queuing

Queue Name: comm_a2p_campaignCheck

Environment Flag: QM_COMMUNICATION_A2P_CAMPAIGN=true (in index.js)

๐Ÿ”„ Complete Processing Flowโ€‹

sequenceDiagram
participant CRON as Cron Schedule<br/>(every 6 hrs)
participant SERVICE as Campaign Status<br/>Service
participant ACCOUNT_DB as Accounts DB
participant QUEUE as Campaign Check<br/>Queue
participant TWILIO as Twilio A2P API

CRON->>SERVICE: campaignStatusCheck()
SERVICE->>ACCOUNT_DB: Aggregate accounts where:<br/>campaignStatus = 'IN_PROGRESS'
ACCOUNT_DB-->>SERVICE: List of accounts

loop Each account
SERVICE->>QUEUE: Add job: {account}
end

loop Each queued account
QUEUE->>TWILIO: GET /Services/{serviceSid}/UsAppToPerson/{campaignSid}<br/>Fetch campaign status
TWILIO-->>QUEUE: Campaign details:<br/>campaignStatus, etc.

QUEUE->>ACCOUNT_DB: Update account:<br/>twilio_account.a2p.messaging_campaign = campaign

alt Campaign status = 'VERIFIED'
QUEUE->>QUEUE: Calculate next renewal:<br/>current_date + 30 days
QUEUE->>ACCOUNT_DB: Set next_renewal field
end
end

๐Ÿ“ Source Filesโ€‹

1. Cron Initializationโ€‹

File: queue-manager/crons/communication/a2p/campaign.js

Purpose: Schedule campaign status check every 6 hours

Cron Pattern: * */6 * * * (every 6 hours, at minute 0)

Initialization:

const { campaignStatusCheck } = require('../../../services/communication/a2p');
const cron = require('node-cron');
const logger = require('../../../utilities/logger');

let inProgress = false;
exports.start = async () => {
try {
cron.schedule('* */6 * * *', async () => {
if (!inProgress) {
inProgress = true;
await campaignStatusCheck();
inProgress = false;
}
});
} catch (err) {
logger.error({ initiator: 'QM/communication/a2p/campaign-status', error: err });
}
};

In-Progress Lock: Prevents overlapping executions.

Execution Times: 00:00, 06:00, 12:00, 18:00 (every 6 hours)

2. Service Processingโ€‹

File: queue-manager/services/communication/a2p.js (campaignStatusCheck export)

Purpose: Find accounts with campaigns awaiting approval and queue for status check

Key Features:

  • Simple aggregation matching 'IN_PROGRESS' campaigns
  • Batch job queuing with exponential backoff retry
  • Promise.all for parallel queuing

Main Service Function:

const campaignStatusCheck = require('../../queues/communication/a2p/campaign');
const Account = require('../../models/account');
const logger = require('../../utilities/logger');

exports.campaignStatusCheck = async () => {
try {
let accounts = await Account.aggregate([
{
$match: {
'twilio_account.a2p.messaging_campaign.campaignStatus': 'IN_PROGRESS',
},
},
{
$project: {
twilio_account: 1,
},
},
]);

if (accounts.length) {
let queue = await campaignStatusCheck.start();
await Promise.all(
accounts.map(async a => {
try {
await queue.add(
{
account: a,
},
{
attempts: 5,
backoff: {
type: 'exponential',
delay: 60000,
},
removeOnComplete: true,
},
);
} catch (err) {
logger.error({
initiator: 'QM/communication/a2p/campaign-status/add-queue',
error: err,
});
}
}),
);
}
} catch (err) {
logger.error({ initiator: 'QM/communication/a2p/campaign-status/service', error: err });
}
};

Query: Matches accounts with campaignStatus: 'IN_PROGRESS'

Campaign Statuses:

  • IN_PROGRESS - Campaign submitted, awaiting approval
  • VERIFIED - Campaign approved, ready for use
  • REJECTED - Campaign rejected by Twilio

3. Queue Processing (THE STATUS CHECK LOGIC)โ€‹

File: queue-manager/queues/communication/a2p/campaign.js

Purpose: Check campaign status and set renewal date when verified

Key Functions:

  • Fetch latest campaign status from Twilio
  • Update database with current status
  • Calculate and set renewal date (30 days) for verified campaigns

Complete Processor:

const mongoose = require('mongoose');
const QueueWrapper = require('../../../common/queue-wrapper');
const logger = require('../../../utilities/logger');
const Account = require('../../../models/account');
const a2p = require('../twilio/services/a2p');

const processCb = async (job, done) => {
try {
const { account } = job.data;

const metadata = {
twilio_creds: {
sid: account.twilio_account?.sid,
token: account.twilio_account?.authToken,
},
};

/**
* START: Check messaging campaign status
*/
let campaign = await a2p.getCampaignStatus(
metadata,
account.twilio_account.a2p.messaging_campaign.messagingServiceSid,
account.twilio_account.a2p.messaging_campaign.sid,
);

if (campaign.campaignStatus == 'VERIFIED') {
/** TODO
* Set twilio_account.a2p.next_renewal = new Date() + 1 month;
*/
let nextRenewal = new Date();
nextRenewal.setMonth(nextRenewal.getMonth() + 1);
await Account.findOneAndUpdate(
{
'twilio_account.sid': metadata.twilio_creds.sid,
},
{
$set: {
'twilio_account.a2p.next_renewal': nextRenewal,
},
},
);
}
/**
* END: Check messaging campaign status
*/

return done();
} catch (err) {
done(err);
}
};

const completedCb = async job => {};

let queue;

exports.start = async () => {
try {
if (!queue)
queue = QueueWrapper(`comm_a2p_campaignCheck`, 'global', { processCb, completedCb });
return Promise.resolve(queue);
} catch (err) {
logger.error({
initiator: 'QM/communication/a2p/campaign-check/queue',
error: err,
message: `Error while starting queue`,
});
}
};

4. Twilio A2P Service Utilitiesโ€‹

File: queue-manager/queues/communication/twilio/services/a2p.js (getCampaignStatus)

Purpose: Fetch campaign status from Twilio and update database

Function:

const getCampaignStatus = async (metadata, service, id) => {
try {
const client = Twilio(metadata.twilio_creds.sid, metadata.twilio_creds.token);
const campaign = await client.messaging.v1.services(service).usAppToPerson(id).fetch();
await Account.findOneAndUpdate(
{
'twilio_account.sid': metadata.twilio_creds.sid,
},
{
$set: {
'twilio_account.a2p.messaging_campaign': JSON.parse(JSON.stringify(campaign)),
},
},
);
return Promise.resolve(campaign);
} catch (err) {
return Promise.reject(err);
}
};

Parameters:

  • metadata: Twilio credentials
  • service: Messaging service SID
  • id: Campaign SID

Returns: Campaign object with latest status

๐Ÿ—„๏ธ Collections Usedโ€‹

_accountsโ€‹

  • Operations: Aggregate, Update
  • Model: shared/models/account.js
  • Usage Context: Store A2P campaign status and renewal tracking

Key Fields (twilio_account.a2p object):

  • messaging_campaign: Object - A2P campaign data
    • sid: Campaign SID
    • messagingServiceSid: Associated service SID
    • campaignStatus: 'IN_PROGRESS' | 'VERIFIED' | 'REJECTED'
    • Other campaign metadata (use case, samples, etc.)
  • next_renewal: Date - Next campaign renewal date (set to current date + 30 days)

๐Ÿ”ง Job Configurationโ€‹

Cron Scheduleโ€‹

'* */6 * * *'; // Every 6 hours (at minute 0)

Frequency: 4 times per day (00:00, 06:00, 12:00, 18:00)

Rationale: Campaign approval can take hours to days; 6-hour checks balance timeliness with API efficiency.

Queue Settingsโ€‹

QueueWrapper(`comm_a2p_campaignCheck`, 'global', {
processCb,
completedCb,
});

Queue Name: comm_a2p_campaignCheck

Concurrency: Default (1)

Job Options:

{
attempts: 5,
backoff: {
type: "exponential",
delay: 60000 // Start with 60-second delay
},
removeOnComplete: true
}

Exponential Backoff: 60s, 120s, 240s, 480s, 960s (up to 16 minutes total)

๐Ÿ“‹ Processing Logic - Detailed Flowโ€‹

1. Campaign Status Queryโ€‹

Service Aggregation:

{
$match: {
'twilio_account.a2p.messaging_campaign.campaignStatus':'IN_PROGRESS'
}
}

Matches: Only accounts with campaigns awaiting approval.

2. Fetch Campaign Statusโ€‹

Twilio API Call:

GET https://messaging.twilio.com/v1/Services/{serviceSid}/UsAppToPerson/{campaignSid}

Response Fields:

  • campaignStatus: Current status ('IN_PROGRESS' | 'VERIFIED' | 'REJECTED')
  • sid: Campaign SID
  • messagingServiceSid: Associated service SID
  • Other campaign metadata

Database Update (always executed):

await Account.findOneAndUpdate(
{ 'twilio_account.sid': metadata.twilio_creds.sid },
{
$set: {
'twilio_account.a2p.messaging_campaign': campaign,
},
},
);

Effect: Updates database with latest campaign status regardless of value.

3. Renewal Date Calculationโ€‹

Condition: campaign.campaignStatus == 'VERIFIED'

Calculation:

let nextRenewal = new Date();
nextRenewal.setMonth(nextRenewal.getMonth() + 1);

Example:

  • Current date: October 13, 2025
  • Next renewal: November 13, 2025

Database Update:

await Account.findOneAndUpdate(
{ 'twilio_account.sid': metadata.twilio_creds.sid },
{
$set: {
'twilio_account.a2p.next_renewal': nextRenewal,
},
},
);

Note: The TODO comment suggests this is the intended behavior (not a bug).

4. Campaign Status Transitionsโ€‹

Typical Flow:

  1. Initial State: campaignStatus: 'IN_PROGRESS' (set by brand check job)
  2. Twilio Review: Twilio reviews campaign (hours to days)
  3. Status Update: This job fetches updated status every 6 hours
  4. Verified: Campaign approved โ†’ campaignStatus: 'VERIFIED'
  5. Renewal Set: next_renewal set to 30 days in future
  6. No Longer Queued: Account no longer matches query (status not 'IN_PROGRESS')

Rejected Flow:

  1. Initial State: campaignStatus: 'IN_PROGRESS'
  2. Twilio Review: Campaign rejected
  3. Status Update: This job fetches updated status โ†’ campaignStatus: 'REJECTED'
  4. No Renewal Set: Renewal date not set for rejected campaigns
  5. No Longer Queued: Account no longer matches query (status not 'IN_PROGRESS')

๐Ÿšจ Error Handlingโ€‹

Common Error Scenariosโ€‹

Campaign Not Foundโ€‹

Scenario: Campaign SID invalid or deleted

Handling: Twilio API error thrown, job retries with exponential backoff (5 attempts)

Impact: Campaign status not updated, retry on next attempt

Invalid Credentialsโ€‹

Scenario: Twilio credentials invalid or expired

Handling: Authentication error thrown, job retries

Impact: All jobs for account fail until credentials fixed

API Rate Limitingโ€‹

Scenario: Too many Twilio API calls

Handling: Rate limit error thrown, exponential backoff delays retries

Impact: Job delayed but eventually succeeds

Database Update Failureโ€‹

Scenario: MongoDB connection issue, validation error

Handling: Error thrown, job retries

Impact: Campaign status not saved, retry on next attempt

Failed Job Callbackโ€‹

Note: No explicit failedCb defined - relies on default Bull error handling.

Completed Job Callbackโ€‹

const completedCb = async job => {};

Action: No-op (empty function)

๐Ÿ“Š Monitoring & Loggingโ€‹

Success Loggingโ€‹

Service Level:

  • No explicit success logging

Queue Level:

  • No explicit success logging

Error Loggingโ€‹

Cron Level:

  • Error in cron initialization

Service Level:

  • Error aggregating accounts
  • Error queuing individual accounts

Queue Level:

  • Error starting queue

Performance Metricsโ€‹

  • Campaign Status Check: 1-2 seconds (Twilio API call)
  • Database Update: less than 1 second
  • Total Job Time: 2-3 seconds per account

๐Ÿ”— Integration Pointsโ€‹

Triggers This Jobโ€‹

  • Cron Schedule: Every 6 hours (no external triggers)
  • Brand Check Job: Creates campaigns with 'IN_PROGRESS' status

External Dependenciesโ€‹

  • Twilio A2P API: Campaign status endpoint
  • Twilio Credentials: Stored in twilio_account.sid and twilio_account.authToken

Jobs That Depend On Thisโ€‹

  • Campaign Renewal: Monitors next_renewal date for renewal processing
  • Number Assignment: Waits for 'VERIFIED' status to assign phone numbers
  • SMS Sending: Requires 'VERIFIED' campaign for 10DLC compliance
  • Campaign Dashboard: Displays current campaign status

โš ๏ธ Important Notesโ€‹

Side Effectsโ€‹

  • โš ๏ธ Status Updates: Campaign status changes affect SMS sending capability
  • โš ๏ธ Renewal Date: Sets 30-day renewal date (triggers renewal workflow)
  • โš ๏ธ Database Writes: Updates messaging_campaign object on every check

Performance Considerationsโ€‹

  • Low Frequency: 6-hour interval minimizes API calls and costs
  • Sequential Processing: One account at a time prevents rate limiting
  • Exponential Backoff: Handles temporary API failures gracefully
  • Job Cleanup: removeOnComplete prevents queue bloat

Business Logicโ€‹

Why Every 6 Hours?

  • Campaign approval takes hours to days (not minutes)
  • Frequent checks (e.g., every minute) waste API calls
  • 6-hour interval balances timeliness with efficiency
  • 4 checks per day ensure updates within reasonable timeframe

Why 30-Day Renewal?

  • Twilio requires monthly campaign renewals for compliance
  • 30-day window allows time for renewal processing
  • Renewal job uses this date to trigger renewal workflow

Why Update Status Always?

  • Even if not verified, status might change to rejected
  • Ensures database reflects current Twilio state
  • Allows monitoring of rejected campaigns

Why Only Check 'IN_PROGRESS'?

  • 'VERIFIED' campaigns no longer need status checks
  • 'REJECTED' campaigns are terminal (no status changes)
  • Reduces unnecessary API calls

Maintenance Notesโ€‹

  • Renewal Period: 30 days hardcoded (consider environment variable)
  • Cron Schedule: 6 hours hardcoded
  • Status Values: 'IN_PROGRESS', 'VERIFIED', 'REJECTED' expected
  • Job Cleanup: removeOnComplete: true prevents indefinite retention
  • TODO Comment: Indicates renewal logic is as intended (not incomplete)

Code Quality Issuesโ€‹

Issue 1: TODO Comment

/** TODO
* Set twilio_account.a2p.next_renewal = new Date() + 1 month;
*/
let nextRenewal = new Date();
nextRenewal.setMonth(nextRenewal.getMonth() + 1);
await Account.findOneAndUpdate(...);

Issue: TODO comment suggests incomplete work, but logic is implemented correctly.

Suggestion: Remove TODO comment or clarify:

// Set next renewal date to 30 days from now
let nextRenewal = new Date();
nextRenewal.setMonth(nextRenewal.getMonth() + 1);

Issue 2: Hardcoded Renewal Period

nextRenewal.setMonth(nextRenewal.getMonth() + 1);

Issue: 30-day period hardcoded (not configurable).

Suggestion: Use environment variable:

const renewalDays = parseInt(process.env.A2P_RENEWAL_DAYS || '30');
nextRenewal.setDate(nextRenewal.getDate() + renewalDays);

Issue 3: No Logging of Status Changes

if (campaign.campaignStatus == 'VERIFIED') {
// No logging before database update
await Account.findOneAndUpdate(...);
}

Suggestion: Add logging for visibility:

if (campaign.campaignStatus == 'VERIFIED') {
logger.log({
initiator: 'QM/communication/a2p/campaign-check',
message: 'Campaign verified, setting renewal date',
account_id: account._id,
campaign_sid: campaign.sid,
next_renewal: nextRenewal,
});
await Account.findOneAndUpdate(...);
}

Issue 4: Edge Case - Month Overflow

nextRenewal.setMonth(nextRenewal.getMonth() + 1);

Issue: If current date is January 31, adding 1 month results in February 28/29 (not March 3).

Impact: Minor date discrepancy, but JavaScript Date handles this natively.

Suggestion: Use days instead of months for precision:

nextRenewal.setDate(nextRenewal.getDate() + 30);

๐Ÿงช Testingโ€‹

Manual Triggerโ€‹

# Via API (if QM_HOOKS=true)
POST http://localhost:6002/api/trigger/communication/a2p/campaign

Simulate Campaign Approvalโ€‹

const Account = require('./models/account');

// Set campaign to IN_PROGRESS
await Account.findByIdAndUpdate(accountId, {
'twilio_account.a2p.messaging_campaign': {
sid: 'CNXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX',
messagingServiceSid: 'MGXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX',
campaignStatus: 'IN_PROGRESS',
},
});

console.log('Campaign set to IN_PROGRESS, waiting for status check');

// Manually trigger status check or wait for next cron run
// (In real scenario, Twilio would approve campaign externally)

Verify Renewal Date Setโ€‹

// After campaign verification (simulate by updating Twilio's side)
// Wait for next cron run (up to 6 hours)

// Check account for renewal date
const account = await Account.findById(accountId);
console.log('Campaign status:', account.twilio_account.a2p.messaging_campaign?.campaignStatus);
console.log('Next renewal:', account.twilio_account.a2p.next_renewal);

// Verify renewal date is ~30 days in future
const daysUntilRenewal =
(account.twilio_account.a2p.next_renewal - new Date()) / (1000 * 60 * 60 * 24);
console.log('Days until renewal:', Math.round(daysUntilRenewal)); // Should be ~30

Test Status Transitionsโ€‹

// Test REJECTED status
await Account.findByIdAndUpdate(accountId, {
'twilio_account.a2p.messaging_campaign.campaignStatus': 'IN_PROGRESS',
$unset: {
'twilio_account.a2p.next_renewal': '',
},
});

// Simulate Twilio rejecting campaign (external)
// After status check, verify no renewal date set

const account = await Account.findById(accountId);
console.log('Campaign status:', account.twilio_account.a2p.messaging_campaign?.campaignStatus);
console.log('Next renewal:', account.twilio_account.a2p.next_renewal); // Should be undefined/null

Monitor Queue Processingโ€‹

# Watch logs during campaign checking
tail -f logs/queue-manager.log | grep "a2p"

# Expected outputs (minimal logging in current implementation):
# Error logs only (if errors occur)

Test Renewal Date Calculationโ€‹

// Test edge case: January 31 + 1 month
const testDate = new Date('2025-01-31');
testDate.setMonth(testDate.getMonth() + 1);
console.log('Jan 31 + 1 month:', testDate.toISOString());
// Result: 2025-02-28 or 2025-03-02 (depending on JavaScript engine)

// Better approach using days
const testDate2 = new Date('2025-01-31');
testDate2.setDate(testDate2.getDate() + 30);
console.log('Jan 31 + 30 days:', testDate2.toISOString());
// Result: 2025-03-02 (consistent)

Job Type: Scheduled with Account-Level Queuing
Execution Frequency: Every 6 hours
Average Duration: 2-3 seconds per account
Status: Active

๐Ÿ’ฌ

Documentation Assistant

Ask me anything about the docs

Hi! I'm your documentation assistant. Ask me anything about the docs!

I can help you with:
- Code examples
- Configuration details
- Troubleshooting
- Best practices

Try asking: How do I configure the API?
09:31 AM