> Adsly docs index: https://adsly.pro/docs/llms.txt · every page in one file: https://adsly.pro/docs/llms-full.txt · OpenAPI: https://adsly.pro/docs/api/openapi.yaml

# Manage campaigns

Change a campaign's text, link, bid and daily cap; show it only on chosen days and hours; pause and resume; add or withdraw budget; copy; delete — one campaign at a time or up to 1,000 with a bulk call.

All calls here need a key with **Manage campaigns** and Manager (or owner) access to the cabinet. They act on one campaign, `(account_id, ad_id)`: pass `account_id` in the body or query when the key reaches several cabinets. Every change appears in the campaign's history in the panel marked **🔌 API**.

| Call | What it does | Answers |
|---|---|---|
| `PATCH /v1/campaigns/{ad_id}` | change text, link, bid, cap, schedule, on/off | right away |
| `POST /v1/campaigns/{ad_id}/pause` | switch off | right away |
| `POST /v1/campaigns/{ad_id}/resume` | switch on | right away |
| `POST /v1/campaigns/{ad_id}/budget` | add or withdraw budget | right away |
| `DELETE /v1/campaigns/{ad_id}` | delete | right away |
| `POST /v1/campaigns/{ad_id}/copy` | 1–100 copies | 202 + [task](https://adsly.pro/docs/api/tasks.md) |
| `POST /v1/campaigns/bulk` | one action on up to 1,000 campaigns | 202 + task |

Direct answers return the updated [campaign object](https://adsly.pro/docs/api/campaigns.md) in `data`.

## Edit

`PATCH /v1/campaigns/{ad_id}` — send only what changes.

```bash
curl -s -X PATCH https://app.adsly.pro/api/v1/campaigns/90412 \
  -H "X-API-Key: $ADSLY_API_KEY" -H "Content-Type: application/json" \
  -d '{ "account_id": 53, "cpm": 1.8, "text": "Daily market notes in 3 minutes." }'
```

| Field | Meaning |
|---|---|
| `title`, `text`, `promote_url` | Creative and link. |
| `cpm` | New bid, greater than 0. |
| `daily_budget` | Daily cap. |
| `views_per_user` | 1–4. |
| `active` | `true` / `false` — same as resume / pause. |
| `schedule` | Days and hours to show the ad; `null` removes it — see [Schedule](#schedule). |
| `media`, `picture`, `button`, `website_name` | As in [create](https://adsly.pro/docs/api/create-campaigns.md). |

**What goes to Telegram's review.** Changing `cpm`, `active` or `schedule` doesn't — they are applied at once and the campaign keeps running. Any other field (`title`, `text`, `promote_url`, `media`, `picture`, `button`, `website_name`, `views_per_user`, `daily_budget`) is saved through Telegram's full ad form, and Telegram reviews the ad again: it shows `In Review` until approved. Send bid, on/off and schedule changes on their own, not together with those fields.

Targeting (`channels`, `countries`, …) can't change on an existing ad — Telegram doesn't allow it. Create a new campaign with the new targeting, or [copy](#copy) and adjust.

`meta` in the response:

| Field | Meaning |
|---|---|
| `status_warning` | The fields saved, but Telegram didn't apply the on/off switch (for example, no budget). |
| `status_intent_only` | Your on/off choice is stored and applies when Telegram can (in review, no budget). |
| `status_already_set` | The campaign was already in the state you asked for. |

A bid below Telegram's minimum for this targeting returns `400 PLATFORM_REJECTED` with `param: "cpm"`, `min_cpm` and `currency`.

## Pause and resume

```bash
curl -s -X POST "https://app.adsly.pro/api/v1/campaigns/90412/pause?account_id=53" -H "X-API-Key: $ADSLY_API_KEY"
curl -s -X POST "https://app.adsly.pro/api/v1/campaigns/90412/resume?account_id=53" -H "X-API-Key: $ADSLY_API_KEY"
```

`meta.intent_only: true` means Telegram can't act on it yet — the campaign is in review, declined, or out of budget — and your choice is stored: it runs as soon as it can. When the status really changes, your [status webhook](https://adsly.pro/docs/api/webhooks.md) fires.

## Schedule

Show a campaign only on chosen weekdays and hours — for example weekdays 9:00–18:00, or evenings only. Outside those hours Telegram doesn't show the ad. Setting, changing or removing a schedule doesn't switch the campaign on or off — `status` and `active` stay as they are. It works on every cabinet type.

```bash
curl -s -X PATCH https://app.adsly.pro/api/v1/campaigns/90412 \
  -H "X-API-Key: $ADSLY_API_KEY" -H "Content-Type: application/json" \
  -d '{ "account_id": 53, "schedule": {
        "days": { "mon": [9,10,11,12,13,14,15,16,17], "tue": [9,10,11,12,13,14,15,16,17],
                  "wed": [9,10,11,12,13,14,15,16,17], "thu": [9,10,11,12,13,14,15,16,17],
                  "fri": [9,10,11,12,13,14,15,16,17] },
        "timezone": "+03:00" } }'
```

| Field | Meaning |
|---|---|
| `days` | `mon`, `tue`, `wed`, `thu`, `fri`, `sat`, `sun` — each holds the hours (0–23) the ad runs that day. `9` means 09:00–09:59, so `[9, …, 17]` is 9:00–18:00. A day you leave out, or send as `[]`, doesn't run. At least one hour in total. |
| `timezone` | Whose clock the hours follow. `"viewer"` — each viewer's own local time; only for audience targeting (`target_type: "users"`). Otherwise a UTC offset: `"+03:00"`, `"-05:00"`, `"+05:30"`. The offsets Telegram offers: whole hours from `-12:00` to `+14:00`, plus `-09:30`, `-03:30`, `-02:30`, `+03:30`, `+04:30`, `+05:30`, `+05:45`, `+06:30`, `+08:45`, `+09:30`, `+10:30`, `+12:45`, `+13:45`. `"UTC+3"` and `"+3"` are accepted too. |

`"schedule": null` removes the schedule — the ad runs around the clock again.

**A campaign with no budget can't take a schedule.** Telegram accepts a schedule only on a campaign that can run, so on a `Stopped` campaign with nothing left you get `400 PLATFORM_REJECTED` with `param: "schedule"` and nothing is changed — add budget first, then set the schedule. A paused (`On Hold`) campaign takes it fine.

The campaign object returns the schedule in the same shape, always with all seven days and the offset written as `"+03:00"` — so you can read it, change a day and send it back. `GET /v1/campaigns/{ad_id}` gives the current schedule even if it was changed directly in Telegram — see [Freshness](https://adsly.pro/docs/api/campaigns.md). Hours outside 0–23, an unknown day, an empty schedule, `"viewer"` on a channel/bot/search ad or an offset Telegram doesn't offer → `400 VALIDATION_ERROR` with the exact field in `param` (for example `schedule.days.mon`).

[Copies](#copy) and recreated campaigns keep the schedule of the campaign they come from. To put one schedule on many campaigns at once, use the bulk action `set_schedule`.

## Budget

`POST /v1/campaigns/{ad_id}/budget` moves money between the cabinet balance and the campaign. Requires an `Idempotency-Key`.

```bash
curl -s -X POST https://app.adsly.pro/api/v1/campaigns/90412/budget \
  -H "X-API-Key: $ADSLY_API_KEY" -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{ "account_id": 53, "action": "add", "amount": 10 }'
```

| `action` | `amount` | Effect |
|---|---|---|
| `add` | required | Cabinet balance → campaign. |
| `withdraw` | required | Campaign → cabinet balance. More than the campaign has → `400 INSUFFICIENT_AD_BALANCE`. |
| `withdraw_all` | — | Everything left on the campaign → cabinet balance. |

`meta.moved` is what actually moved; `meta.partial` is `true` when less than asked could be withdrawn. Money never leaves the cabinet through the API.

## Copy

`POST /v1/campaigns/{ad_id}/copy` creates copies with the same targeting; anything you send overrides the original. Requires an `Idempotency-Key`. Returns a task whose `ad_ids` are the copies.

```json
{ "account_id": 53, "copies": 3, "text": "Variant B — shorter", "cpm": 2, "budget": 5 }
```

| Field | Meaning |
|---|---|
| `copies` | 1–100, default 1. |
| `title`, `text`, `promote_url`, `cpm`, `budget`, `media`, `picture`, `views_per_user` | Overrides for every copy. `budget`: `0` or at least the cabinet's `min_budget`. |
| `schedule` | Copies keep the original's [schedule](#schedule); send one to replace it, or `null` for none. |
| `group_id` / `new_group_name` | Put the copies in a group. |

## Delete

```bash
curl -s -X DELETE "https://app.adsly.pro/api/v1/campaigns/90412?account_id=53" -H "X-API-Key: $ADSLY_API_KEY"
```

```json
{ "success": true, "data": { "ad_id": 90412, "account_id": 53, "deleted": true } }
```

A deleted campaign that had views stays in your stats with `status: "Deleted"`; one that never ran is removed. Deleting again returns `404 CAMPAIGN_NOT_FOUND`.

## Bulk

`POST /v1/campaigns/bulk` applies one action to up to 1,000 campaigns of one cabinet. Requires an `Idempotency-Key`. Returns a task.

```bash
curl -s -X POST https://app.adsly.pro/api/v1/campaigns/bulk \
  -H "X-API-Key: $ADSLY_API_KEY" -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{ "account_id": 53, "action": "set_cpm", "cpm": 1.2, "ad_ids": [90412, 90413, 90414] }'
```

| `action` | Extra field | Effect |
|---|---|---|
| `pause` | — | switch off |
| `resume` | — | switch on |
| `add_budget` | `amount` | add to each campaign |
| `withdraw_budget` | `amount` | withdraw from each campaign |
| `withdraw_all_budget` | — | withdraw everything from each campaign |
| `set_cpm` | `cpm` | set the same bid on each |
| `set_schedule` | `schedule` | set the same [schedule](#schedule) on each; `"schedule": null` removes it |
| `delete` | — | delete each |

Every `ad_id` must be a live campaign of that cabinet; otherwise nothing is queued and you get `400 UNKNOWN_AD_IDS` with the offenders in `missing`. Duplicates are ignored. The task's `errors` lists campaigns Telegram refused — with `set_schedule`, also campaigns with no budget and campaigns where `"timezone": "viewer"` can't apply because they don't target an audience.

## Recipe: pause campaigns whose cost per action is too high

```python
import os, uuid, requests

API = "https://app.adsly.pro/api/v1"
H = {"X-API-Key": os.environ["ADSLY_API_KEY"]}
ACCOUNT, MAX_CPA = 53, 4.0

stats = requests.get(f"{API}/stats", params={"period": "1d"}, headers=H, timeout=60).json()["data"]
# cost per action = spend / actions, computed here so the rule means exactly that
expensive = [int(ad_id) for ad_id, s in stats.get(str(ACCOUNT), {}).items()
             if s["actions"] > 0 and s["spent"] / s["actions"] > MAX_CPA]

if expensive:
    r = requests.post(f"{API}/campaigns/bulk",
                      headers={**H, "Idempotency-Key": str(uuid.uuid4())},
                      json={"account_id": ACCOUNT, "action": "pause", "ad_ids": expensive},
                      timeout=60)
    print(r.status_code, r.json()["data"]["id"])
```

---
Page: https://adsly.pro/docs/api/manage-campaigns/ · Updated: 2026-10-08
