Ads
Pause, reactivate or rebudget campaigns, ad sets and ads on a schedule when their metrics cross a threshold, or get a webhook when they do.
An ad rule watches one ad account for you. On a schedule, it checks every campaign, ad set or ad against a set of conditions ("cost per conversion above 25 over the last 7 days") and acts on each one that matches: pause it, turn it back on, change its daily budget, or send you an ad_rule.triggered webhook. Rules run on Meta, Google, TikTok, LinkedIn, Pinterest, X and Reddit.
#Create a rule safely
#Begin with a template
GET /v1/ads/rules/templates gives you finished rules to copy, for jobs such as cutting off ad sets that lose money, giving winners more budget, pausing ads that bring no conversions, spotting tired creative and warning you when spend jumps. A template is already a full rule body. Its platforms field names the ad networks it suits; null means every network.
Take one, set name, accountId and adAccountId, and adjust the thresholds. Money amounts are in the ad account's currency.
#Create it switched off
This rule checks every hour and pauses any ad set that spent at least 50 in the last 7 days at a cost per conversion above 25:
curl -X POST https://api.pozy.ai/v1/ads/rules \
-H "Authorization: Bearer $POZY_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Pause expensive ad sets",
"accountId": "66b2e19d8c3f5a7e9d0b1c2d",
"adAccountId": "act_1234567890",
"level": "adset",
"conditions": { "all": [
{ "metric": "cpa", "window": "last_7d", "op": ">", "value": 25 },
{ "metric": "spend", "window": "last_7d", "op": ">=", "value": 50 }
] },
"actions": [{ "type": "pause", "maxChangesPerDay": 10 }, { "type": "notify" }],
"schedule": { "everyMinutes": 60 },
"cooldownMinutes": 1440,
"enabled": false
}'const res = await fetch("https://api.pozy.ai/v1/ads/rules", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.POZY_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
name: "Pause expensive ad sets",
accountId: "66b2e19d8c3f5a7e9d0b1c2d",
adAccountId: "act_1234567890",
level: "adset",
conditions: {
all: [
{ metric: "cpa", window: "last_7d", op: ">", value: 25 },
{ metric: "spend", window: "last_7d", op: ">=", value: 50 },
],
},
actions: [{ type: "pause", maxChangesPerDay: 10 }, { type: "notify" }],
schedule: { everyMinutes: 60 },
cooldownMinutes: 1440,
enabled: false,
}),
});
const data = await res.json();
if (!res.ok) throw new Error(data.error);
console.log(data.rule.id);import os, requests
resp = requests.post(
"https://api.pozy.ai/v1/ads/rules",
headers={"Authorization": f"Bearer {os.environ['POZY_API_KEY']}"},
json={
"name": "Pause expensive ad sets",
"accountId": "66b2e19d8c3f5a7e9d0b1c2d",
"adAccountId": "act_1234567890",
"level": "adset",
"conditions": {"all": [
{"metric": "cpa", "window": "last_7d", "op": ">", "value": 25},
{"metric": "spend", "window": "last_7d", "op": ">=", "value": 50},
]},
"actions": [{"type": "pause", "maxChangesPerDay": 10}, {"type": "notify"}],
"schedule": {"everyMinutes": 60},
"cooldownMinutes": 1440,
"enabled": False,
},
)
print(resp.json()["rule"]["id"])The response (201) is the rule, with lastRunAt and nextRunAt. enabled defaults to true, so send false explicitly while you test.
#Preview against today's numbers
POST /v1/ads/rules/{ruleId}/preview evaluates the conditions on current metrics and acts on nothing. It returns every object with matched, the actual value of each condition, and the time zone and currency used.
curl -X POST https://api.pozy.ai/v1/ads/rules/66f9a0b1c2d3e4f5a6b7c8d9/preview \
-H "Authorization: Bearer $POZY_API_KEY"{
"evaluatedAt": "2026-10-30T09:00:00.000Z",
"today": "2026-10-30",
"timezone": "America/New_York",
"timezoneAssumed": false,
"currency": "USD",
"matched": 1,
"objects": [
{ "id": "120210000000000001", "name": "Retargeting, 7 day", "campaignId": "120210000000000000", "matched": true, "conditions": [ { "metric": "cpa", "actual": 31.4 }, { "metric": "spend", "actual": 188.2 } ] }
]
}#Turn it on
curl -X PATCH https://api.pozy.ai/v1/ads/rules/66f9a0b1c2d3e4f5a6b7c8d9 \
-H "Authorization: Bearer $POZY_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "enabled": true }'#Rule fields
| Field | Meaning |
|---|---|
accountId |
Your ads connection (metaads, googleads, tiktokads, linkedinads, pinterestads, xads or redditads), not an account you post with. |
adAccountId |
Which ad account of that connection to watch. Take it from GET /v1/ads/accounts?accountId=...; it has to be one the connection syncs. |
level |
campaign, adset or ad. Each object at that level is checked on its own. |
campaignIds |
Optional. Only these campaigns, or the ad sets and ads inside them. |
conditions.all |
1 to 10 conditions. All must hold for an object to match. |
actions |
What to do with each match (below). |
schedule.everyMinutes |
60, 180, 360, 720 or 1440. |
cooldownMinutes |
How long an object rests after the rule acts on it. Default 1440 (a day), up to 43,200. |
enabled |
Whether it runs. Default true. |
#Conditions
Each condition checks one metric, measured over one time window, against a threshold:
{ "metric": "roas", "window": "last_3d", "op": "<", "value": 1.5 }- Metrics:
spend,impressions,clicks,conversions,cpc,cpm,cpa,roas,reach,frequency, andctras a percentage. - Windows:
today,yesterday,last_3d,last_7d,last_14d,last_30d. A day is a day in the ad account's time zone, and the multi-day windows count today. - Operators:
>,>=,<,<=. reachandfrequencyneed de-duplicated reach, which only Meta and TikTok provide. On other networks they return400.- A rate with nothing to divide by never matches. A
cpacondition skips objects with no conversions, so pair it with aspendfloor as in the example.
#Actions
type |
What it does |
|---|---|
notify |
Sends the ad_rule.triggered webhook with the objects that matched. Subscribe an endpoint first (see Webhooks), or a notify-only rule only leaves run history. |
pause |
Pauses the object. |
activate |
Turns back on what this rule paused, once the cooldown has passed. It never touches objects you paused yourself. |
adjust_budget |
Changes the daily budget. direction is increase or decrease; percent is up to 100 up or 90 down; limit is the ceiling for increases and the floor for decreases. Not on level: "ad". |
Combine notify with no more than one changing action: pause, activate or adjust_budget. Those three accept maxChangesPerDay, from 1 to 100, which caps how many objects the rule may change in a rolling 24 hours, counting all its runs. Objects past the cap are skipped with daily_limit_reached and picked up on a later run. A rule that changes objects must also have a cooldownMinutes no shorter than its schedule.
#What a rule did
GET /v1/ads/rules/{ruleId}/runs shows a rule's runs over the past 31 days, latest first. For the workspace as a whole, GET /v1/ads/rules/activity lists each change that any rule made.
outcome |
What happened |
|---|---|
triggered |
The rule acted on the objects that matched, listed in actedObjectIds. |
no_match |
No object passed all the conditions. |
cooldown |
Objects matched, but all were resting. |
failed |
Metrics could not be read, every action failed, or a notify-only rule could not queue its webhook. error says why. |
Each run's evaluation.actions[] records status (done, skipped or failed) per object, with a reason on skips and previousDailyBudget and newDailyBudget on budget changes. evaluation.notification.status is done once the webhook is queued.
#The webhook
ad_rule.triggered fires when a run of a rule with a notify action acts on at least one object.
objects[]lists only the objects the run acted on. Objects in cooldown are left out, and a run where every match is resting sends nothing.objects[].conditionsholds every condition again, withactualset to the object's real value.objects[].actionsrecords what the rule changed on the object; a rule that only notifies leaves it empty.run.today,run.timezoneandrun.currencytell you which calendar day and currency the numbers used.run.timezoneAssumedistruewhen the ad account has no known time zone and UTC was used.
The full payload is in the events reference.
#How rules behave
- Metrics lag. Rules read the metrics Pozy syncs from each network, which can trail the network's own dashboard.
- Batch size. A run changes 20 objects at most; any further matches are handled by the next run.
- Failed actions get no cooldown, so the next run retries them.
- Your own pauses.
activateonly restores what the rule paused. If you turn an object on and off again between two runs, the rule cannot tell your pause from its own. - Google budgets live on campaigns, so
adjust_budgeton Google needslevel: "campaign". - Ads access. If the workspace loses ads access, every rule pauses until it returns.
- Free allowance. Past the free allowance of live ads without a payment method, reactivations and budget increases are skipped with
ads_allowance_exceeded. Pauses and decreases keep working. - Disabled rules stop running until you set
enabled: trueagain.DELETE /v1/ads/rules/{ruleId}removes a rule for good.
#Errors you will see
| Status and code | Cause | Fix |
|---|---|---|
403 ad_account_not_selected |
The ad account is not one the connection syncs | Add it with adAccountIds on the connect call, which replaces the selection. |
400 invalid_field_value on cooldownMinutes |
The rule changes objects and its cooldown is shorter than its schedule | Set cooldownMinutes to schedule.everyMinutes or more. |
400 invalid_field_value on actions |
A budget change at ad level, or on ad sets in Google | Switch to level: "campaign", or adset on networks other than Google. |
400 invalid_field_value on conditions |
reach or frequency on a network other than Meta or TikTok |
Pick a different metric. |
403 ads_allowance_exceeded |
A reactivating or budget-raising rule past the free live-ads allowance | Add a payment method. |
404 resource_not_found |
No such rule in this workspace | Check the ruleId. |
#Endpoints
| Endpoint | What it does |
|---|---|
GET /v1/ads/rules |
List ad rules |
POST /v1/ads/rules |
Create an ad rule |
GET /v1/ads/rules/templates |
List ready-made ad rules |
GET /v1/ads/rules/activity |
List every change ad rules made |
GET /v1/ads/rules/{ruleId} |
Get an ad rule |
PATCH /v1/ads/rules/{ruleId} |
Update an ad rule |
DELETE /v1/ads/rules/{ruleId} |
Delete an ad rule |
POST /v1/ads/rules/{ruleId}/preview |
Preview an ad rule |
GET /v1/ads/rules/{ruleId}/runs |
List an ad rule's runs |
#Webhook events
| Event | When it fires |
|---|---|
ad_rule.triggered |
Ad rule triggered. |