โ 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:
- Cron Initialization:
queue-manager/crons/communication/a2p/campaign.js - Service Processing:
queue-manager/services/communication/a2p.js(campaignStatusCheck) - 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 approvalVERIFIED- Campaign approved, ready for useREJECTED- 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 credentialsservice: Messaging service SIDid: 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 datasid: Campaign SIDmessagingServiceSid: Associated service SIDcampaignStatus: '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 SIDmessagingServiceSid: 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:
- Initial State:
campaignStatus: 'IN_PROGRESS'(set by brand check job) - Twilio Review: Twilio reviews campaign (hours to days)
- Status Update: This job fetches updated status every 6 hours
- Verified: Campaign approved โ
campaignStatus: 'VERIFIED' - Renewal Set:
next_renewalset to 30 days in future - No Longer Queued: Account no longer matches query (status not 'IN_PROGRESS')
Rejected Flow:
- Initial State:
campaignStatus: 'IN_PROGRESS' - Twilio Review: Campaign rejected
- Status Update: This job fetches updated status โ
campaignStatus: 'REJECTED' - No Renewal Set: Renewal date not set for rejected campaigns
- 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.sidandtwilio_account.authToken
Jobs That Depend On Thisโ
- Campaign Renewal: Monitors
next_renewaldate for renewal processing - Number Assignment: Waits for 'VERIFIED' status to assign phone numbers
Related Featuresโ
- 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_campaignobject 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:
removeOnCompleteprevents 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: trueprevents 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