If you’re trying to get Meta Ads Management approval in Meta App and facing Meta App Review rejection, let me save you days (or weeks) of frustration.
Because honestly —
👉 Most tutorials online are incomplete
👉 Meta documentation is not practical
👉 And rejection reasons are confusing
I went through multiple rejections before finally getting approved — and in this blog, I’ll share the exact things that actually worked.
😤 Why Meta Ads Management Approval Gets Rejected (Real Reasons)
I got multiple Meta App Review rejections while applying for Ads Management approval, and the reasons were not clearly explained.
After multiple rejections, I realized something important:
👉 Meta does NOT reject randomly
👉 They reject because you’re missing specific signals
And in my case, there were two major problems.
⚠️ Problem 1: Your Demo Video is NOT What Meta Wants
Most developers (including me initially) think:
“I just need to show login + UI overview”
❌ That’s WRONG.
❌ What I Was Doing Earlier
- Showing login flow
- Showing “Connect Facebook” button
- Showing dashboard UI (like ad accounts list)
👉 But I was NOT showing what happens inside the system
✅ What Meta Actually Wants
Meta App Review is not UI-based — it is logic-based.
They want to see:
- How you are using the User Access Token
- How you are calling the Facebook Graph API
- How permissions like
ads_management,ads_readare used - How data flows behind the scenes
💡 The Solution That Worked for Me
To fix this, I created a dummy backend flow specifically for the demo video
Instead of just showing UI, I demonstrated:
- User authentication flow
- Access token generation
- Token being used in API requests
- API calls to Meta endpoints
- Data being fetched and returned
👉 Even if some data was dummy — the flow was real and clear
🎥 Important Tips for Demo Video (Very Critical)
If you want approval, follow this strictly:
- ✅ Language must be English (UI + Voiceover)
- ✅ Explain each step clearly (don’t assume anything)
- ✅ Show how API is being used
- ✅ Show token usage explicitly
- ✅ Explain why each permission is required
- ❌ Don’t skip backend explanation
🔥 Pro Insight (Most People Miss This)
Meta reviewers are NOT trying to understand your UI…
👉 They are checking:
“Is this app actually using Meta APIs correctly?”
Once I understood this — my approach completely changed.
⚠️ Problem 2: “Low API Usage” & “High Error Rate” Rejection
This one is VERY frustrating and not clearly documented anywhere.
I got rejected multiple times with errors like:
- “Not enough successful API calls”
- “High error rate”
👉 And honestly, this is where most developers get stuck.
🤯 What Meta is Actually Checking
Meta internally tracks:
- Are you actually using their APIs?
- How frequently are you calling APIs?
- What is your success vs failure ratio?
- Are your endpoints stable?
👉 If your app looks inactive or unstable → ❌ Rejected
💡 The Solution That Worked for Me
To solve this, I built a dedicated script to generate API activity
This script:
- Makes real Meta Graph API calls
- Fetches businesses, ad accounts, catalogs
- Uses actual user access tokens
- Maintains a high success rate
- Builds consistent API usage history

⏱️ What I Did Exactly
- Ran this script daily for ~10 days
- Ensured delays between calls (to avoid rate limits)
- Made sure most calls were successful
👉 Then I reapplied → ✅ Got Approved
🧠 The Script That Helped Me Get Approved (Real Strategy)
Here is the actual script I used to solve the “low API usage” and “high error rate” problem and reach Meta’s expected activity level.
👉 This is not just a simple API caller — it’s a controlled high-volume API execution system.
To fix the Meta Ads Management approval rejection, I created a script that generates consistent Meta Graph API usage and improves success rate.
🔧 Full Script
// /src/app/api/cron/meta-ads-ping/route.js
// CRON JOB - Meta Ads Sandbox API caller
// Goal: Call until rate limit is hit (max 1500 calls per run)
import { NextResponse } from "next/server";
import { connectDB } from "@/app/lib/connectDB";
import CronLog from "@/models/CronLog";
// ══════════════════════════════════════════════════════════════════════════════
// CONFIG
// ══════════════════════════════════════════════════════════════════════════════
const CONFIG = {
DEVELOPER_TOKEN: "EAATV5XhFwvMBQ5f5WJ7gZBcUCfQUnneJ*********************************************************************************3K6ZAeQgFtS",
SANDBOX_AD_ACCOUNT_ID: "****************",
API_VERSION: "v19.0",
MAX_CALLS_PER_RUN: 1500, // hard ceiling — stop at 1500 or rate limit
USAGE_THRESHOLD: 70, // pause if acc_id_util_pct exceeds this %
DELAY_BETWEEN_CALLS: 500, // 0.5s between calls (faster to hit limit)
};
const BASE_URL = `https://graph.facebook.com/${CONFIG.API_VERSION}`;
// ══════════════════════════════════════════════════════════════════════════════
// HELPERS
// ══════════════════════════════════════════════════════════════════════════════
function sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
function parseUsageHeaders(headers) {
const adUsage = headers.get("x-ad-account-usage");
if (adUsage) {
try {
const parsed = JSON.parse(adUsage);
return {
source: "x-ad-account-usage",
acc_id_util_pct: parsed.acc_id_util_pct ?? 0,
resetSeconds: null,
};
} catch { /* fall through */ }
}
const bizUsage = headers.get("x-business-use-case-usage");
if (bizUsage) {
try {
const parsed = JSON.parse(bizUsage);
const entries = Object.values(parsed).flat();
const pct = entries.reduce((max, e) => Math.max(max, e.call_count ?? 0), 0);
const reset = entries.reduce((max, e) => Math.max(max, e.estimated_time_to_regain_access ?? 0), 0);
return {
source: "x-business-use-case-usage",
acc_id_util_pct: pct,
resetSeconds: reset > 0 ? reset : null,
};
} catch { /* fall through */ }
}
return { source: null, acc_id_util_pct: 0, resetSeconds: null };
}
// ══════════════════════════════════════════════════════════════════════════════
// OPERATIONS
// ══════════════════════════════════════════════════════════════════════════════
const OPERATIONS = [
{
name: "fetch_account_details",
endpoint: () => `${CONFIG.SANDBOX_AD_ACCOUNT_ID}`,
params: {
fields: "id,name,account_status,currency,timezone_name,spend_cap,amount_spent",
},
},
{
name: "fetch_campaigns",
endpoint: () => `${CONFIG.SANDBOX_AD_ACCOUNT_ID}/campaigns`,
params: {
fields: "id,name,status,objective,created_time",
limit: "5",
},
},
{
name: "fetch_adsets",
endpoint: () => `${CONFIG.SANDBOX_AD_ACCOUNT_ID}/adsets`,
params: {
fields: "id,name,status,daily_budget,billing_event",
limit: "5",
},
},
{
name: "fetch_ads",
endpoint: () => `${CONFIG.SANDBOX_AD_ACCOUNT_ID}/ads`,
params: {
fields: "id,name,status,created_time",
limit: "5",
},
},
{
name: "fetch_insights",
endpoint: () => `${CONFIG.SANDBOX_AD_ACCOUNT_ID}/insights`,
params: {
fields: "impressions,clicks,spend",
date_preset: "last_7d",
limit: "5",
},
},
];
// ══════════════════════════════════════════════════════════════════════════════
// STOP REASONS — clear enum for why the loop ended
// ══════════════════════════════════════════════════════════════════════════════
const STOP_REASON = {
RATE_LIMITED: "RATE_LIMITED", // code 17 / code 4 / usage threshold
INVALID_TOKEN: "INVALID_TOKEN", // code 190
MAX_CALLS_REACHED: "MAX_CALLS_REACHED", // hit 1500 ceiling
FATAL_ERROR: "FATAL_ERROR", // unexpected crash
};
// ══════════════════════════════════════════════════════════════════════════════
// SINGLE API CALL — NO retries; caller decides what to do with errors
// ══════════════════════════════════════════════════════════════════════════════
async function callMetaAPI(operation) {
const url = new URL(`${BASE_URL}/${operation.endpoint()}`);
url.searchParams.set("access_token", CONFIG.DEVELOPER_TOKEN);
Object.entries(operation.params || {}).forEach(([k, v]) =>
url.searchParams.set(k, v)
);
let response;
try {
response = await fetch(url.toString());
} catch (networkErr) {
throw { type: "NETWORK", message: networkErr.message };
}
const usage = parseUsageHeaders(response.headers);
const json = await response.json();
if (json.error) {
const { code, message, type, fbtrace_id } = json.error;
throw { type: "META_ERROR", code, subType: type, message, fbtrace_id, usage };
}
return { data: json, usage };
}
// ══════════════════════════════════════════════════════════════════════════════
// MAIN CRON HANDLER
// ══════════════════════════════════════════════════════════════════════════════
export async function GET(req) {
const startTime = Date.now();
let cronLog = null;
try {
// ── Security ──────────────────────────────────────────────────────────────
const authHeader = req.headers.get("authorization");
const apiKey = req.headers.get("x-api-key");
const validCronAuth = authHeader === `Bearer ${process.env.CRON_SECRET}`;
const validApiKey = apiKey === process.env.CRON_API_KEY;
if (!validCronAuth && !validApiKey) {
console.error("❌ Unauthorized cron access attempt");
return NextResponse.json({ ok: false, error: "Unauthorized" }, { status: 401 });
}
// ── Config guard ──────────────────────────────────────────────────────────
if (!CONFIG.DEVELOPER_TOKEN || !CONFIG.SANDBOX_AD_ACCOUNT_ID) {
return NextResponse.json(
{ ok: false, error: "Missing Meta API credentials" },
{ status: 500 }
);
}
await connectDB();
console.log(`\n🔔 ===== META ADS INFINITE PING STARTED =====`);
console.log(`Time: ${new Date().toISOString()}`);
console.log(`Ad Account: ${CONFIG.SANDBOX_AD_ACCOUNT_ID}`);
console.log(`Max ceiling: ${CONFIG.MAX_CALLS_PER_RUN} calls`);
console.log(`Strategy: Fire until rate-limited or ceiling reached\n`);
cronLog = await CronLog.create({
jobName: "meta-ads-ping",
status: "running",
startedAt: new Date(),
});
// ── Results tracker ───────────────────────────────────────────────────────
const results = {
processed: 0,
succeeded: 0,
failed: 0,
stopReason: null,
stopAt: null, // call number where we stopped
lastUsage: null, // final throttle snapshot
details: [], // keep last 50 to avoid huge payloads
};
// ── Infinite loop — exits via break ───────────────────────────────────────
for (let i = 0; i < CONFIG.MAX_CALLS_PER_RUN; i++) {
const operation = OPERATIONS[i % OPERATIONS.length];
results.processed++;
// Log every 50 calls to keep noise down
if (i === 0 || (i + 1) % 50 === 0) {
console.log(`📡 Call ${i + 1} → ${operation.name}`);
}
try {
const result = await callMetaAPI(operation);
results.succeeded++;
results.lastUsage = result.usage;
// Keep a rolling window of last 50 detail records
if (results.details.length < 50) {
results.details.push({
call: i + 1,
operation: operation.name,
status: "success",
usage: result.usage,
});
}
// ── Check usage threshold AFTER a successful call ──────────────────
if (result.usage.acc_id_util_pct >= CONFIG.USAGE_THRESHOLD) {
console.warn(`\n🛑 Usage threshold hit: ${result.usage.acc_id_util_pct}% ≥ ${CONFIG.USAGE_THRESHOLD}%`);
results.stopReason = STOP_REASON.RATE_LIMITED;
results.stopAt = i + 1;
break;
}
} catch (err) {
results.failed++;
// Capture last known usage from the error if available
if (err.usage) results.lastUsage = err.usage;
results.details.push({
call: i + 1,
operation: operation.name,
status: "failed",
errorType: err.type,
errorCode: err.code,
message: err.message,
});
// ── Code 17 → rate limited → STOP (no retry — this is the target) ──
if (err.code === 17) {
console.warn(`\n🛑 Rate limited — code 17 after ${i + 1} calls. Mission accomplished.`);
results.stopReason = STOP_REASON.RATE_LIMITED;
results.stopAt = i + 1;
break;
}
// ── Code 4 → app-level rate limit → STOP ──────────────────────────
if (err.code === 4) {
console.warn(`\n🛑 App-level rate limit — code 4 after ${i + 1} calls.`);
results.stopReason = STOP_REASON.RATE_LIMITED;
results.stopAt = i + 1;
break;
}
// ── Code 190 → invalid token → fatal stop ─────────────────────────
if (err.code === 190) {
console.error(`\n🚨 Invalid/expired token — halting. Check DEVELOPER_TOKEN.`);
results.stopReason = STOP_REASON.INVALID_TOKEN;
results.stopAt = i + 1;
break;
}
// ── Any other error → log and keep firing ─────────────────────────
console.error(`⚠️ Non-fatal error on call ${i + 1} (${err.type} / code ${err.code}) — continuing`);
}
// Small polite delay between calls
await sleep(CONFIG.DELAY_BETWEEN_CALLS);
}
// ── If loop exhausted without a break ────────────────────────────────────
if (!results.stopReason) {
results.stopReason = STOP_REASON.MAX_CALLS_REACHED;
results.stopAt = CONFIG.MAX_CALLS_PER_RUN;
console.log(`\n🏁 Ceiling of ${CONFIG.MAX_CALLS_PER_RUN} calls reached without hitting rate limit.`);
}
// ── Finalize ──────────────────────────────────────────────────────────────
const executionTime = Date.now() - startTime;
await CronLog.findByIdAndUpdate(cronLog._id, {
status: "completed",
completedAt: new Date(),
executionTime,
results,
});
console.log(`\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━`);
console.log(`✅ ===== META ADS INFINITE PING COMPLETED =====`);
console.log(`Execution time: ${executionTime}ms`);
console.log(`Stop reason: ${results.stopReason}`);
console.log(`Stopped at: call #${results.stopAt}`);
console.log(`Succeeded: ${results.succeeded}`);
console.log(`Failed: ${results.failed}`);
console.log(`Last usage: ${results.lastUsage?.acc_id_util_pct ?? "N/A"}%`);
console.log(`━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n`);
return NextResponse.json({
ok: true,
message: "Meta ads infinite ping completed",
timestamp: new Date().toISOString(),
executionTime: `${executionTime}ms`,
stats: results,
});
} catch (error) {
console.error("❌ Cron fatal error:", error);
if (cronLog) {
await CronLog.findByIdAndUpdate(cronLog._id, {
status: "failed",
completedAt: new Date(),
executionTime: Date.now() - startTime,
error: { message: error.message, stack: error.stack },
});
}
return NextResponse.json(
{ ok: false, error: error.message, timestamp: new Date().toISOString() },
{ status: 500 }
);
}
}
export async function POST(req) {
return GET(req);
}
💡 What This Script Actually Does
This script is designed to:
- Continuously call Meta Ads Graph API
- Rotate between multiple endpoints (campaigns, ads, insights, etc.)
- Maintain a high success rate
- Stop intelligently when rate limit is reached
- Generate up to 1500 API calls per run
⚙️ Core Idea Behind This Script
Instead of randomly hitting APIs…
👉 I created a structured loop system that:
- Fires API requests in sequence
- Tracks usage from Meta headers
- Detects rate limits automatically
- Stops at the right time
👉 This ensures:
High activity + Controlled execution + Low error rate
🔧 Key Features of the Script
1. 🔁 Infinite Loop with Smart Stop Conditions
The script runs in a loop:
- Executes up to 1500 API calls
- Cycles through different operations:
- Account details
- Campaigns
- Ad sets
- Ads
- Insights
👉 This creates a real usage pattern, not fake spam
2. 📊 Rate Limit Detection (VERY IMPORTANT)
The script reads Meta headers like:
x-ad-account-usagex-business-use-case-usage
👉 Then calculates:
- API usage %
- When to stop safely
3. 🛑 Intelligent Stop Conditions
Instead of crashing, the script stops when:
- Rate limit is hit (code 17 or 4)
- Usage threshold exceeds defined %
- Max calls (1500) are reached
- Token becomes invalid
👉 This is exactly what Meta expects:
Stable and controlled API usage
4. 📉 Error Handling Strategy
- Handles Meta errors properly
- Continues on non-critical errors
- Stops only on critical failures
👉 This helps maintain:
- Low error rate
- High success ratio
5. ⏱️ Delay Between Calls
Each request has a small delay (~500ms)
👉 This prevents:
- Sudden spikes
- API throttling
- Suspicious behavior
6. 🧾 Logging & Tracking
The script:
- Logs each API call
- Tracks success & failure
- Stores execution data in DB
- Maintains last usage snapshot
👉 This helps in:
- Debugging
- Proving API usage to Meta
🎯 What APIs Are Being Called
This script actively uses:
/act_{ad_account}→ account details/campaigns/adsets/ads/insights
👉 This directly uses Meta Ads Management APIs, which is critical for approval.
🚀 Why This Script Works (Most Important Part)
Meta is NOT just checking:
“Did you submit correctly?”
They are checking:
- Are you actively using Ads APIs?
- Do you have consistent traffic?
- Is your success rate good?
👉 This script proves:
- ✅ Real API usage
- ✅ High volume (~1500 calls)
- ✅ Controlled execution
- ✅ Stable system behavior
⏱️ My Execution Strategy
- Ran this script daily using CRON
- Let it hit near rate limit naturally
- Maintained consistent usage for ~10 days
👉 Then reapplied for review → ✅ Approved
🔥 Pro Insight
The goal is NOT to avoid rate limits…
👉 The goal is to:
Reach rate limit in a controlled and healthy way
That signals to Meta:
“This app is actively used and production-ready”
🎯 Why This Works
This ensures:
- ✅ High number of API calls
- ✅ Good success rate
- ✅ Real usage pattern
- ✅ Better trust from Meta
🚀 My Final Working Strategy (Step-by-Step)
If you want to get Meta Ads Management approval, follow this:
- Fix your use case (clear explanation)
- Create demo video (show backend logic)
- Provide test credentials + steps
- Generate API usage using script
- Wait 7–10 days
- Apply for review
⚠️ Common Mistakes (Avoid These)
- ❌ Only showing UI in demo video
- ❌ No API/token explanation
- ❌ No API usage history
- ❌ High error rate
- ❌ Requesting unnecessary permissions
- ❌ Poor or unclear demo video
🎯 Final Thoughts
Getting Meta Ads Management Standard & Advanced Access approval is not about:
“Submitting the form correctly”
It’s about:
Proving that your app is real, functional, and actively using the Facebook Graph API
Once you understand this — approval becomes much easier and predictable.
💬 If You’re Stuck…
If you’re building:
- SaaS platform
- Ads management tool
- Automation system
And facing Meta App Review rejections…
👉 You’re not alone — I’ve been there.
Feel free to reach out — I can guide you based on real experience (not theory).