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 |
POST /v1/campaigns/bulk | one action on up to 1,000 campaigns | 202 + task |
Direct answers return the updated campaign object in data.
Edit
PATCH /v1/campaigns/{ad_id} — send only what changes.
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. |
media, picture, button, website_name | As in create. |
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 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
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 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.
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. 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 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.
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.
{ "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; send one to replace it, or null for none. |
group_id / new_group_name | Put the copies in a group. |
Delete
curl -s -X DELETE "https://app.adsly.pro/api/v1/campaigns/90412?account_id=53" -H "X-API-Key: $ADSLY_API_KEY"
{ "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.
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 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
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"]) Updated 2026-10-08