Delivery limits via API
The daily/weekly/monthly/total limit grid on advertisers, campaigns and banners, reading limits back, resetting counters, bulk reads, site and placement limits, and per-action conversion limits.
Delivery limits over REST: the grid of daily, weekly, monthly and total caps on impressions,
clicks, conversions, opportunities and budget, how to read them back and reset their counters,
the bulk reads, the supply-side limits of sites and placements, and per-action conversion limits.
Examples append $AUTH, the hash-authentication parameters
(an X-Api-Key header works the same) — see the API overview for authentication.
Who this is for
Developers who script delivery control for a network — network managers, and publisher or advertiser users whose role holds the permissions named below. The calls behave the same for every role; what differs is which entities and dimensions your role may change.
Limits
Delivery limits (impressions, clicks, conversions, budget, ad opportunities — daily, weekly, monthly and total) exist on advertisers, campaigns and banners with an identical endpoint set (sites and placements: see below), and an identical 27 parameters at every level — twenty caps and seven settings:
GET /rest-api/campaign/{campaignId}/limits.do read
POST /rest-api/campaign/{campaignId}/limits/set.do create if absent, then merge the supplied fields
POST /rest-api/campaign/{campaignId}/limits/update.do merge the supplied fields; limits must already exist
POST /rest-api/campaign/{campaignId}/limits/reset.do reset named counters
DELETE /rest-api/campaign/{campaignId}/limits.do delete the limit record outright
All fifteen operations gate on two things: UPDATE access to the entity (through campaign
sharing) and the Manage Capping & Limitations permission — enabled to change anything, enabled
or read-only to read. They authenticate like every other REST call: hash, session, or the
X-Api-Key header.
The limit grid
Twenty of the parameters are one grid: four periods × five metrics, each a cap you can set. Every cap has a counter next to it, which a read returns but a write cannot set.
| impressions | clicks | conversions | opportunities | budget | |
|---|---|---|---|---|---|
| daily | dailyImpressionsLimit | dailyClicksLimit | dailyConversionsLimit | dailyOpportunitiesLimit | dailyBudgetLimit |
| weekly | weeklyImpressionsLimit | weeklyClicksLimit | weeklyConversionsLimit | weeklyOpportunitiesLimit | weeklyBudgetLimit |
| monthly | monthlyImpressionsLimit | monthlyClicksLimit | monthlyConversionsLimit | monthlyOpportunitiesLimit | monthlyBudgetLimit |
| total | totalImpressionsLimit | totalClicksLimit | totalConversionsLimit | totalOpportunitiesLimit | totalBudgetLimit |
Counts are whole numbers capped at 4,000,000,000; budget is a decimal capped at
9,999,999,999.999. Across every cell, 0 and “omitted” both mean no cap — the serving
check is limit > 0 && counter >= limit, so a cap of zero never stops delivery, and a cap is
reached at equal to, not greater than.
Drop the Limit suffix and you have the twenty counter names —
dailyImpressions, weeklyClicks, totalBudget and so on. Daily counters roll over every
day, weekly on Monday (ISO week), monthly on the 1st.
Corner case: the counters are read-only through the API on all five entity levels (advertiser, campaign, banner, site, placement). A counter you send to
set.doorupdate.dois not applied — it is listed inignoredFieldswith the reason, and the rest of the call still saves.reset.dois the only way to change one (to zero); otherwise counters advance as delivery is recorded.
The other seven parameters set the run dates and pacing:
| Parameter | Format | Purpose |
|---|---|---|
startDate, endDate | yyyy-MM-dd-HH-mm or yyyy-MM-dd HH:mm, GMT+0 | The limiting period. Parsing is strict — 2026-13-01-00-00 is a 400, not a rolled-over date, and any other shape is refused naming both accepted formats |
removeStartDateLimit, removeEndDateLimit | boolean | Clear an existing start/end date. Wins over a startDate/endDate sent in the same call |
evenPacing | boolean | Spread delivery through the day rather than front-loading |
useLocalTime | boolean | Read the startDate / endDate you send in timezoneOffset instead of GMT+0 and answer them back in it. It changes only how the run dates are entered and shown — nothing in delivery reads the flag, so it does not move when the daily counters roll over |
timezoneOffset | integer, minutes east of UTC, -840 to 840 | The offset the run dates are read in. Only has an effect when useLocalTime is on; the dates you send are converted back to UTC for storage, so startDate=2026-07-01-12-00 with timezoneOffset=90 is stored as 10:30 |
A read answers the limits with "limitsSet":true; an entity that has none answers 200 with
{"campaignId":2001,"limitsSet":false} (advertiserId / bannerId on the other two) rather
than an empty body, so “no limits” and “no such entity” (404) do not look alike. The
read carries every cap set.do accepts with its current counter — weekly and monthly ones
included — plus evenPacing and useLocalTime.
Each cap row of the console Limits card has its own permission — Daily Budget,
Weekly Budget, Monthly Budget, Budget (total), and Daily / Weekly / Monthly / Total
Impressions Limit, …Clicks Limit, …Conversions Limit and …Opportunities Limit for every period — and the advertiser,
campaign and banner endpoints follow it the same way the console does: a read leaves out
every cap and counter whose permission your role holds neither enabled nor readonly. A
set.do or update.do that changes a cap without its permission enabled leaves that cap
as stored and lists it in ignoredFields, its ignoredFieldReasons entry naming the
permission; the rest of the request is saved. A cap sent with the value it already has is
no change and needs no permission, so a client that re-sends its whole limit set keeps
working. A reset.do of a counter without its permission is a 403 naming it. Caps you
do hold are not held back by the ones you don’t: a role without Daily Clicks Limit can
still set a daily budget.
curl -X POST "https://<your-network-domain>/rest-api/campaign/2001/limits/set.do?$AUTH" \
-d "totalBudgetLimit=10000.00" \
-d "dailyBudgetLimit=500.00" \
-d "dailyImpressionsLimit=50000" \
-d "evenPacing=true" \
-d "startDate=2026-07-01-00-00" \
-d "endDate=2026-07-31-23-59"
All fields, with descriptions and an example request:
POST /rest-api/campaign/{campaignId}/limits/set.do
— the advertiser and banner levels are listed next to it.
Corner case: neither endpoint is a full replace. Both merge: a field you leave out keeps its stored value. The only difference between them is that
set.docreates the limit record when the entity has none —update.doanswers404instead. When in doubt, useset.do.
Note:
useLocalTimefollows the same merge rule — leaving it out keeps the entity’s current setting rather than switching local-time resets off. WithuseLocalTime=true, thestartDate/endDateof that request are read intimezoneOffsetand stored in UTC; dates you do not send are never moved, so switching local time on, or changing a cap on a local-time limit, leaves the run dates where they were. A read answers UTC unless you pass it the sametimezoneOffset: on a local-time limit the dates then come back in that offset, as the console shows them, withtimezoneOffsetechoed.
Corner case: a shorter period’s cap may not exceed a longer one’s —
dailyClicksLimitabovemonthlyClicksLimitis a400with per-field errors — unless the longer one is0, which means uncapped and so constrains nothing.
Warning:
DELETE …/limits.dois destructive. It does not “disable” limiting: it deletes the limit record, thresholds and counters together, and a followingGETreports no limits at all. There is no disable-but-keep state — re-enabling means sending every value again withset.do. To stop limiting temporarily, zero the individual caps or move the end date instead.
Corner case: the entity carries two no-conversion limits — “stop after N hours with no conversions” and “stop after N impressions with no conversions” — that the REST layer does not expose.
set.doandupdate.dohave no parameter for them, a read does not return them, and they can only be configured from the UI; their counters can be reset over the API (NO_CONVERSIONS_HOURS,NO_CONVERSIONS_IMPRESSIONSbelow). They also only bite together: delivery stops when both thresholds are crossed, not either. The same goes for request limits on advertisers, campaigns and banners — the reset endpoint acceptsTOTAL_REQUESTSand friends there, but there is no*RequestsLimitparameter, because request limits are a site and placement cap (see below).
Reading limits back
GET …/limits.do returns the limit as stored: id, startDate, endDate, evenPacing,
useLocalTime, limitsSet: true, and every cap of the grid with its counter — the weekly and
monthly ones included (weeklyImpressionsLimit and weeklyImpressions, monthlyBudgetLimit and
monthlyBudget, …). A cap or counter that was never set is left out of the body, and so is one
whose permission your role does not hold (see above). On a limit held in local time, pass the
timezoneOffset the dates were entered in to read them back in it; without it they come back in
UTC. The two no-conversion thresholds are not returned, and an entity with no limits answers
200 with limitsSet: false, as above — never 204 or 404.
Resetting counters
reset.do takes a required, repeatable limitCounters parameter — there is no “reset
everything” default. Twenty-nine values, matched case-sensitively:
TOTAL_IMPRESSIONS TOTAL_CLICKS TOTAL_CONVERSIONS TOTAL_OPPORTUNITIES TOTAL_REVENUE TOTAL_REQUESTS
DAILY_IMPRESSIONS DAILY_CLICKS DAILY_CONVERSIONS DAILY_OPPORTUNITIES DAILY_REVENUE DAILY_REQUESTS
WEEKLY_IMPRESSIONS WEEKLY_CLICKS WEEKLY_CONVERSIONS WEEKLY_OPPORTUNITIES WEEKLY_REVENUE WEEKLY_REQUESTS
MONTHLY_IMPRESSIONS MONTHLY_CLICKS MONTHLY_CONVERSIONS MONTHLY_OPPORTUNITIES MONTHLY_REVENUE MONTHLY_REQUESTS
NO_CONVERSIONS_HOURS NO_CONVERSIONS_IMPRESSIONS
ALL_LIMITS ALL_ACTION_LIMITS ALL
REVENUE is the counter behind the *Budget* parameters — the grid and the reset enum use
different words for the same thing. ALL_LIMITS covers the delivery counters,
ALL_ACTION_LIMITS the per-action ones, and ALL both. An unrecognised name is a 400.
curl -X POST "https://<your-network-domain>/rest-api/campaign/2001/limits/reset.do?$AUTH" \
-d "limitCounters=DAILY_IMPRESSIONS" -d "limitCounters=DAILY_CLICKS"
Warning: a reset is not immediate. The call does not zero the counters directly; it queues a delayed reset that the ad-log flush applies, so the numbers change at the next hourly flush rather than on the response. Do not poll for the new value straight after the call — and expect
ALLandALL_LIMITSin particular to look like no-ops until the flush lands.
Bulk reads
| Method | Path | Parameters |
|---|---|---|
| GET | /rest-api/campaign/limits.do | campaignIds (omit for every accessible campaign), startDateFrom, startDateTo (both yyyy-MM-dd-HH-mm, filtering on the limit’s own start date, inclusive), activeCampaignOnly |
| GET | /rest-api/banners/limits.do | bannerIds — required |
The campaign response is an envelope: limits (present only when non-empty), withNoLimits
(campaigns that matched but have no limit record), and notFound / accessForbidden when some
requested IDs failed. Each is a string (a long list is truncated); the same ids also come back as
arrays in notFoundIds and accessForbiddenIds. When every requested id is unknown or
refused, the call is a 404 or 403 naming the first one instead. Deleted campaigns are always
excluded, and the query is hard-capped at 10,000 rows with no
paging and no total — split large reconciliations by ID batch.
Corner case: a request is checked against the limit as it would stand afterwards, not only against what it sends: an
update.dowith only a daily cap above the stored total, or only anendDatebefore the storedstartDate, is a400naming the field, and nothing is saved. Only the caps and dates a request sends are judged, so an inconsistency already stored does not block an unrelated change.timezoneOffsetis -840 to 840 minutes, andreset.doneeds at least onelimitCountersvalue.
The banner response is a bare JSON array with no envelope. Banners without limits are
skipped silently, but a single unknown ID fails the whole call with 404, and a single banner
you lack UPDATE on fails it with 403 — so one bad ID costs you the entire batch.
Site and placement limits
The Limits card of a site or placement has the same endpoint set, under
/rest-api/site/{siteId}/… and /rest-api/placement/{placementId}/…:
GET /rest-api/site/{siteId}/limits.do read ({"siteId":…, "limitsSet":false} when none)
POST /rest-api/site/{siteId}/limits/set.do create if needed (the card's switch) and apply
POST /rest-api/site/{siteId}/limits/update.do change the fields sent (404 when there are no limits)
POST /rest-api/site/{siteId}/limits/reset.do limitCounters=DAILY_REQUESTS,TOTAL_CLICKS,…
DELETE /rest-api/site/{siteId}/limits.do switch limits off
The supply side caps ad requests (daily/weekly/monthly/totalRequestsLimit), impressions,
clicks and conversions, each daily, weekly, monthly and total (the counters are read back
but, as on the demand side, not writable), and the same startDate/endDate, evenPacing, useLocalTime and timezoneOffset. There
is no budget or opportunity cap on a site or placement — sending one is a 400.
curl -X POST "https://<your-network-domain>/rest-api/placement/3001/limits/set.do?$AUTH" \
-d "dailyRequestsLimit=200000" \
-d "dailyImpressionsLimit=50000"
All fields, with descriptions and an example request:
POST /rest-api/placement/{placementId}/limits/set.do.
Permissions follow the console: a share on the site, Manage Site and Placement Limits to see
(read-only is enough) and to save the card, Manage Capping & Limitations to create or
remove the limits, and each cap’s own inventory permission (Daily Clicks Limit and its
siblings on the inventory side) — a cap you may not see
is left out of the read and named, with the permission it needs, in fieldsHiddenByPermission; writing or resetting one you may not change is a 403 naming
the permission.
Per-action limits
Cap conversions per individual action (goal) rather than in aggregate. Unlike delivery limits, these exist on five entity types.
| Method | Path | Parameters |
|---|---|---|
| GET | /rest-api/per-action/limits/{type}/{entityId}.do | — |
| POST | /rest-api/per-action/add/limit/{type}/{entityId}.do | actionId*, dailyConversionsLimit, totalConversionsLimit |
| POST | /rest-api/per-action/update/limit/{type}/{entityId}.do | actionId*, dailyConversionsLimit, totalConversionsLimit |
| POST | /rest-api/per-action/delete.do | type*, entityId*, ids* (the per-action limit record IDs) |
| POST | /rest-api/per-action/limits/{type}/{entityId}/enable.do | Switch per-action limits on (ADVERTISER, CAMPAIGN, BANNER only) |
| POST | /rest-api/per-action/limits/{type}/{entityId}/disable.do | Switch them off — also deletes the entity’s per-action limits, as in the console |
{type} is BANNER, CAMPAIGN, ADVERTISER, PLACEMENT or SITE, and it is
case-sensitive — campaign is rejected with a 400 before anything else is checked. delete.do takes type, entityId and the limit ids as query parameters, not in the
path like the other three; every id must be a limit of that entity, or it is a 404 naming it
and nothing is deleted.
At least one of the two caps must be present; both absent is refused, as is a negative value, an
unknown actionId (errors.actionId) or a daily cap above a non-zero total (judged against the
cap that will be stored, so a lone dailyConversionsLimit above the stored total is refused too).
As with delivery limits, 0 means uncapped.
Note:
addandupdateboth change only the cap you send; a cap you leave out keeps its stored value, or is0(uncapped) on a new limit.
Corner case:
addis really an upsert. There can be only one limit per (entity, action) pair, so callingaddagain for the sameactionIdchanges the existing record rather than failing. Conversely,updateagainst an action that has no limit yet is a404(Campaign 2001 has no limit for action 10.) — useaddto create one.
Corner case: the limits only cap delivery while the entity’s per-action-limits switch is on.
addswitches it on;updatedoes not, so a limit updated onto an entity whose flag is off stays inert.enable.do/disable.dotoggle it directly forADVERTISER,CAMPAIGNandBANNER—disable.doalso deletes the entity’s per-action limits, as the console does. Those two need Manage Capping & Limitations plus Campaign Per Action Limit and update access to the owning advertiser. The limit calls themselves need the per-action permission of the entity’s side: Campaign Per Action Limit for advertiser, campaign and banner, Inventory Per Action Limit for site and placement.
Corner case: the GET response is
{"success":true,"actionLimits":[…]}, each entry carryingid,entityIdand the four conversion figures — notactionId. Sincedelete.doneeds the recordidand you addressed everything else byactionId, keep the mapping between the two yourself.
Note: a permission failure on
add,updateordeleteis a403carrying{"success": false, "permissionDenied": true}, as onGET.