Fixing a Rejected or Changes-Needed Campaign
Campaign reviews are done by human reviewers, not automated systems. When they need something from you, the campaign status changes to changes_needed or rejected. Here's how to read the feedback and get your campaign approved.
Campaign status lifecycle
stateDiagram-v2
[*] --> created
created --> pending
pending --> in_review
in_review --> active
in_review --> changes_needed
in_review --> rejected
changes_needed --> pending : resubmitThe diagram shows the typical review lifecycle. deactivated and canceled are administrative states reachable from active (and earlier states for canceled); see the list below.
created: you submitted the campaign, Surge has received itpending: queued for submission to the carrier networkin_review: actively being reviewed by TCR and carrier reviewersactive: approved; you can attach phone numbers and send production trafficchanges_needed: reviewer flagged issues; you need to update and resubmitrejected: campaign was rejected; see notes on appealing belowdeactivated: previously active campaign suspended by carriercanceled: you or Surge canceled the campaign
API status values. The campaign status field in API responses uses a condensed set of public values: created, in_review, active, rejected, canceled, deactivated. Both the pending and in_review internal states surface as "in_review" in the API. Both the changes_needed and rejected internal states surface as "rejected". Keep this in mind when polling for status or using the update table below.
Understanding "changes needed" feedback
When a campaign moves to changes_needed, Surge sends you an email with reviewer notes. Read it carefully — reviewers include specific details about what needs to change.
Common patterns and what they mean:
| Reviewer says | What to fix |
|---|---|
| "The website doesn't give much info" | Add clear business description and contact information to your homepage |
| "The opt-in page doesn't show required disclosures" | Add message frequency notice, "Msg&data rates apply", and "Reply STOP to opt out" next to the opt-in checkbox |
| "The sample message contains placeholder text" | Replace {brand name}, {Agent Name}, etc. with actual values |
| "The privacy policy is behind a login" | Move the privacy policy to a publicly accessible URL |
| "Your business email doesn't match the domain" | Use an email at your business domain, not a personal or generic address |
Updating a campaign
To address feedback, update the relevant campaign fields and resubmit. Campaigns can only be updated in certain statuses:
| Status (API value) | Can update? |
|---|---|
created | Yes |
in_review | Usually no: returns campaign_locked (409) once a reviewer picks it up. A brief window exists while the campaign is still queued (internally pending) before review begins. |
rejected | Yes, both "changes needed" and hard-rejected campaigns can be updated. |
canceled | Yes |
active | No: returns campaign_locked (409) |
deactivated | No: returns campaign_locked (409) |
If you try to update while in_review, wait for the review to complete. The reviewer will either approve it, request changes (moving it to changes_needed), or reject it. Only then can you update.
The campaign update endpoint requires the full field set, not a partial body. Send consent_flow, description, message_samples (≥2 items), privacy_policy_url, terms_and_conditions_url, use_cases, and volume together. Missing any field returns a validation_error.
from surge import Surge
surge = Surge() # reads SURGE_API_KEY from environment
campaign = surge.campaigns.update(
"{campaign_id}",
consent_flow=(
"Users enter their phone number during checkout. An unchecked checkbox "
'labeled "I agree to receive text messages from Acme Corp" is present. '
'Required disclosures are shown: "Message frequency varies. Msg&data '
'rates apply. Reply STOP to opt out. View our Privacy Policy at '
'acme.com/privacy."'
),
message_samples=[
"Acme Corp: Your order #12345 has shipped! Track it: https://acme.com/track",
"Acme Corp: Your cart is waiting. Complete your order: https://acme.com/cart. Reply STOP to opt out.",
],
description="Marketing messages to opted-in customers",
privacy_policy_url="https://acme.com/privacy",
terms_and_conditions_url="https://acme.com/terms",
use_cases=["marketing"],
volume="low",
)
print(campaign.status) # "in_review"import Surge from "@surgeapi/node";
const surge = new Surge();
const campaign = await surge.campaigns.update("{campaign_id}", {
consent_flow:
'Users enter their phone number during checkout. An unchecked checkbox ' +
'labeled "I agree to receive text messages from Acme Corp" is present. ' +
'Required disclosures are shown: "Message frequency varies. Msg&data ' +
'rates apply. Reply STOP to opt out. View our Privacy Policy at ' +
'acme.com/privacy."',
message_samples: [
"Acme Corp: Your order #12345 has shipped! Track it: https://acme.com/track",
"Acme Corp: Your cart is waiting. Complete your order: https://acme.com/cart. Reply STOP to opt out.",
],
description: "Marketing messages to opted-in customers",
privacy_policy_url: "https://acme.com/privacy",
terms_and_conditions_url: "https://acme.com/terms",
use_cases: ["marketing"],
volume: "low",
});
console.log(campaign.status); // "in_review"require "surge_api"
surge = SurgeAPI::Client.new
campaign = surge.campaigns.update(
"{campaign_id}",
consent_flow:
'Users enter their phone number during checkout. An unchecked checkbox ' \
'labeled "I agree to receive text messages from Acme Corp" is present. ' \
'Required disclosures are shown: "Message frequency varies. Msg&data ' \
'rates apply. Reply STOP to opt out. View our Privacy Policy at ' \
'acme.com/privacy."',
message_samples: [
"Acme Corp: Your order #12345 has shipped! Track it: https://acme.com/track",
"Acme Corp: Your cart is waiting. Complete your order: https://acme.com/cart. Reply STOP to opt out."
],
description: "Marketing messages to opted-in customers",
privacy_policy_url: "https://acme.com/privacy",
terms_and_conditions_url: "https://acme.com/terms",
use_cases: ["marketing"],
volume: "low"
)
puts campaign.status # "in_review"# Surge.Campaigns.update/3 isn't typed in the Elixir SDK yet — use the raw client.
client = Surge.Client.new(System.get_env("SURGE_API_KEY"))
{:ok, campaign} =
Surge.Client.request(client, :patch, "/campaigns/{campaign_id}",
json: %{
consent_flow:
"Users enter their phone number during checkout. An unchecked checkbox " <>
"labeled \"I agree to receive text messages from Acme Corp\" is present. " <>
"Required disclosures are shown: \"Message frequency varies. Msg&data " <>
"rates apply. Reply STOP to opt out. View our Privacy Policy at " <>
"acme.com/privacy.\"",
message_samples: [
"Acme Corp: Your order #12345 has shipped! Track it: https://acme.com/track",
"Acme Corp: Your cart is waiting. Complete your order: https://acme.com/cart. Reply STOP to opt out."
],
description: "Marketing messages to opted-in customers",
privacy_policy_url: "https://acme.com/privacy",
terms_and_conditions_url: "https://acme.com/terms",
use_cases: ["marketing"],
volume: "low"
}
)
IO.puts(campaign["status"]) # "in_review"curl -X PATCH https://api.surge.app/campaigns/{campaign_id} \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"consent_flow": "Users enter their phone number during checkout. An unchecked checkbox labeled \"I agree to receive text messages from Acme Corp\" is present. Required disclosures are shown: \"Message frequency varies. Msg&data rates apply. Reply STOP to opt out. View our Privacy Policy at acme.com/privacy.\"",
"message_samples": [
"Acme Corp: Your order #12345 has shipped! Track it: https://acme.com/track",
"Acme Corp: Your cart is waiting. Complete your order: https://acme.com/cart. Reply STOP to opt out."
],
"description": "Marketing messages to opted-in customers",
"privacy_policy_url": "https://acme.com/privacy",
"terms_and_conditions_url": "https://acme.com/terms",
"use_cases": ["marketing"],
"volume": "low"
}'After updating a changes_needed, rejected, or canceled campaign, the campaign moves back to pending and then in_review.
Checking campaign status
Poll the campaign endpoint to track the current status:
from surge import Surge
surge = Surge()
campaign = surge.campaigns.retrieve("{campaign_id}")
print(campaign.status)import Surge from "@surgeapi/node";
const surge = new Surge();
const campaign = await surge.campaigns.retrieve("{campaign_id}");
console.log(campaign.status);require "surge_api"
surge = SurgeAPI::Client.new
campaign = surge.campaigns.retrieve("{campaign_id}")
puts campaign.status# Surge.Campaigns.get isn't typed in the Elixir SDK yet — use the raw client.
client = Surge.Client.new(System.get_env("SURGE_API_KEY"))
{:ok, campaign} = Surge.Client.request(client, :get, "/campaigns/{campaign_id}")
IO.puts(campaign["status"])curl https://api.surge.app/campaigns/{campaign_id} \
-H "Authorization: Bearer YOUR_API_KEY"{
"id": "cpn_01jrzhe8d9enptypyx360pcmxl",
"status": "in_review",
"use_cases": ["marketing"],
"volume": "low"
}Or subscribe to the campaign.approved webhook event to get notified the moment a campaign goes active. This avoids polling. See Webhook Events.
Rejected campaigns
A rejected status is more serious than changes_needed. Unlike changes_needed, a rejection doesn't automatically move the campaign back into the queue when you update it — you may need to create a new campaign entirely. Rejections related to spam or misuse may also affect your account's ability to register future campaigns.
If your campaign is rejected:
Read the rejection notice carefully
Reviewers include the specific reason. Don't guess.
Address every listed issue before doing anything else
Partial fixes typically lead to another rejection.
Contact Surge support
Confirm whether to resubmit the existing campaign or start a new one — the right move depends on the reason.
Make sure your opt-in flow, sample messages, and business identity are solid before resubmitting. If you're unsure what went wrong, Avoiding Rejection covers the seven most common patterns.