Ads

Automated ad rules

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
  }'

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
curl -X POST https://api.pozy.ai/v1/ads/rules/66f9a0b1c2d3e4f5a6b7c8d9/preview \
  -H "Authorization: Bearer $POZY_API_KEY"
Response (trimmed)
{
  "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
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:

JSON
{ "metric": "roas", "window": "last_3d", "op": "<", "value": 1.5 }
  • Metrics: spend, impressions, clicks, conversions, cpc, cpm, cpa, roas, reach, frequency, and ctr as 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: >, >=, <, <=.
  • reach and frequency need de-duplicated reach, which only Meta and TikTok provide. On other networks they return 400.
  • A rate with nothing to divide by never matches. A cpa condition skips objects with no conversions, so pair it with a spend floor 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[].conditions holds every condition again, with actual set to the object's real value.
  • objects[].actions records what the rule changed on the object; a rule that only notifies leaves it empty.
  • run.today, run.timezone and run.currency tell you which calendar day and currency the numbers used. run.timezoneAssumed is true when 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. activate only 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_budget on Google needs level: "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: true again. 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.