---
title: "Delivery limits via API"
description: "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."
type: "reference"
audience: [network-manager, admin]
keywords: [api, limits, budget, daily budget, impressions limit, clicks limit, conversions limit, reset counters, per-action limits, evenPacing, curl]
---

Fetch the complete documentation index at: https://epom.com/ad-server/docs/llms.txt. Use this file to discover all available pages before exploring further. Append .md to any documentation page URL to get its markdown version.

# Delivery limits via API

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`](https://epom.com/ad-server/docs/api.md#the-auth-shorthand), the hash-authentication parameters
(an `X-Api-Key` header works the same) — see the [API overview](https://epom.com/ad-server/docs/api.md) 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](#site-and-placement-limits)), 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.do` or `update.do`
> is not applied — it is listed in `ignoredFields` with the reason, and the rest of the call
> still saves. `reset.do` is 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.

```bash
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`](https://epom.com/ad-server/docs/api/limits.md#post-rest-apicampaigncampaignidlimitssetdo)
— 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.do` creates the limit record
> when the entity has none — `update.do` answers `404` instead. When in doubt, use `set.do`.

> **Note:** `useLocalTime` follows the same merge rule — leaving it out keeps the entity's
> current setting rather than switching local-time resets off. With `useLocalTime=true`, the
> `startDate`/`endDate` **of that request** are read in `timezoneOffset` and 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 same `timezoneOffset`: on a local-time limit the dates then come back in that offset,
> as the console shows them, with `timezoneOffset` echoed.

> **Corner case:** a shorter period's cap may not exceed a longer one's — `dailyClicksLimit`
> above `monthlyClicksLimit` is a `400` with per-field errors — unless the longer one is `0`,
> which means uncapped and so constrains nothing.

> **Warning:** `DELETE …/limits.do` is destructive. It does not "disable" limiting: it deletes
> the limit record, thresholds and counters together, and a following `GET` reports no limits at
> all. There is no disable-but-keep state — re-enabling means sending every value again with
> `set.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.do` and `update.do` have 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_IMPRESSIONS` below). 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 accepts
> `TOTAL_REQUESTS` and friends there, but there is no `*RequestsLimit` parameter, because request
> limits are a site and placement cap (see [below](#site-and-placement-limits)).

### 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`.

```bash
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 `ALL` and `ALL_LIMITS` in 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.do` with only a daily cap above the stored total, or
> only an `endDate` before the stored `startDate`, is a `400` naming 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. `timezoneOffset` is -840 to 840 minutes, and `reset.do`
> needs at least one `limitCounters` value.

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`.

```bash
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`](https://epom.com/ad-server/docs/api/limits.md#post-rest-apiplacementplacementidlimitssetdo).

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:** `add` and `update` both change only the cap you send; a cap you leave out keeps its
> stored value, or is `0` (uncapped) on a new limit.

> **Corner case:** `add` is really an upsert. There can be only one limit per
> (entity, action) pair, so calling `add` again for the same `actionId` changes the existing
> record rather than failing. Conversely, `update` against an action that has no limit yet is a
> `404` (*Campaign 2001 has no limit for action 10.*) — use `add` to create one.

> **Corner case:** the limits only cap delivery while the entity's per-action-limits switch is
> on. `add` switches it on; `update` does not, so a limit updated onto an entity whose flag is
> off stays inert. `enable.do` / `disable.do` toggle it directly for `ADVERTISER`, `CAMPAIGN`
> and `BANNER` — `disable.do` also 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 carrying `id`,
> `entityId` and the four conversion figures — **not `actionId`**. Since `delete.do` needs the record `id` and you addressed everything else
> by `actionId`, keep the mapping between the two yourself.

> **Note:** a permission failure on `add`, `update` or `delete` is a `403` carrying
> `{"success": false, "permissionDenied": true}`, as on `GET`.

## Related

- [Targeting, capping and limits via API](https://epom.com/ad-server/docs/api/targeting-capping-limits.md) — the overview
- [API overview](https://epom.com/ad-server/docs/api.md)
- [Targeting rules via API](https://epom.com/ad-server/docs/api/targeting-api.md)
- [Frequency capping via API](https://epom.com/ad-server/docs/api/capping-api.md)
- [Price rules, bid floors and pricing via API](https://epom.com/ad-server/docs/api/price-rules-and-floors-api.md)
- [Generated reference: limits](https://epom.com/ad-server/docs/api/limits.md)
- [Limits (UI guide)](https://epom.com/ad-server/docs/guides/delivery/limits.md)