Command-line interface for the Yandex Direct API.
pip install direct-cli
The GitHub Actions Build workflow builds and checks wheel and source
distributions on Ubuntu, Windows, and macOS with Python 3.11. It runs on pushes
to main, pull requests, and manually via Actions → Build → Run workflow.
Each job installs the wheel and checks CLI help outside the source tree.
Download packages from the run's Artifacts section (direct-cli-<os>).
These are Python packages requiring Python 3.10+, not standalone executables.
The separate Quality workflow runs lint, type checks, and offline tests.
Мастер кампаний has no Yandex Direct API surface at all — it only exists
in the web interface, and must not be confused with UNIFIED_CAMPAIGN (an
unrelated v5 API campaign type already supported by campaigns add/get --type unified_campaign). Browser-backed commands use a saved browser session,
independent of the API token. Set it up with direct auth login browser —
a headless import of the already logged-in Chrome session (no window;
works on macOS/Linux/Windows without opening one). The visible login
window exists only behind explicit direct auth login browser --source manual. Requires the optional browser extra:
pip install "direct-cli[browser]" playwright install chromium direct auth login browser direct auth login browser --source manual direct masters list direct masters list --status archived direct masters get 72349978 direct masters suspend 72349978 direct masters resume 72349978 direct masters archive 72349978 direct masters delete 713337891 direct masters delete 713337891 --yes direct masters update 72349978 --weekly-budget 95000 direct masters update 72349978 --promotion-goal max-clicks --no-directs-helps direct masters update 72349978 --name "Мастер ИЖ Источник Жизни (тёплый)" direct masters update 72349978 --headline "2=Новый заголовок" --text "1=Новый текст" direct masters update 72349978 --image "2=/path/to/banner.png" direct masters adimages get 72349978 direct masters adimages add 72349978 --image-file /path/to/a.png --image-file /path/to/b.png direct masters adimages delete 72349978 --position 2 direct masters adimages delete 72349978 --all direct masters adimages set 72349978 --image-file /path/to/a.png --image-file /path/to/b.png direct masters add https://example.com/ --headline "Заголовок 1" --headline "Заголовок 2" --text "Текст объявления" --region Москва --add-target-action "236386933=150" --weekly-budget 50000 --draft direct masters add https://example.com/ --headline "Заголовок 1" --text "Текст объявления" --region-id 213 --add-target-action "236386933=150" --weekly-budget 50000 --draft direct masters copy 72349978 direct masters copy 72349978 --launch
masters list filters by status with --status
(not-archived/active/stopped/archived/all, default not-archived).
It always reads the logged-in browser session's own account — there is no
--login/agency support for managed clients. Every row also carries the
grid's «Бюджет и стратегия» cell, read from the same response with no extra
page request: Strategy (the grid's raw enum — OPTIMIZE_CLICKS /
OPTIMIZE_CONVERSIONS), WeeklyBudget, and, where the strategy has them,
AvgCpa / GoalId / PayForConversion / IsAutoBudget (plus EndDate);
rows without a strategy (e.g. drafts) report these as null.
masters add creates a new Мастер кампаний. Besides --headline/--text
and one of --region/--region-id, it requires --add-target-action
("goal_id=price", repeatable — the same flag name and syntax as
masters update --add-target-action) and --weekly-budget: Yandex's
create form refuses to submit without at least one Яндекс Метрика conversion
goal, or without a weekly budget, and refuses silently in both cases —
both terminal buttons keep reporting visible/enabled with no aria-disabled,
so a goal-less or budget-less attempt would otherwise fail as an unexplained
timeout after the whole form was filled in. The goals on offer come from the
Metrika counter Yandex auto-discovers from the landing page's domain, so a
domain with no counter installed cannot be used here. There is no sandbox and
no rollback for Мастер кампаний — double-check every flag before running it
for real.
masters update edits a single Мастер кампаний's settings page. It currently
covers the simplest scalar fields (Этап A of a larger, staged rollout — see
direct_cli/browser/masters.py module docstring), the campaign name,
point-replacement of individual headline/ad-text variants (Этап B), and
point-replacement of individual images (Этап D):
--weekly-budget (integer), --promotion-goal
(max-conversions/max-clicks), --directs-helps/--no-directs-helps
(auto-apply recommendations), --name (Название кампании),
--headline/--text (repeatable "N=text", N is the 1-based variant slot
shown on the edit page — 1-5 for headlines, 1-3 for ad text), and --image
(repeatable "N=/path/to/file.png", N is the 1-based position in the
campaign's current image set). At least one flag is required. The settings page is a single form with one save button —
passing only some flags leaves every other field at its current value; it
does not send a partial payload to a separate per-field endpoint. Unlike the
other fields, --name is edited through a separate header modal, not a plain
form input — but it is still persisted only by that same terminal save
button, not by the modal's own "Применить".
--headline/--text replace one existing variant at a time rather than
the whole variant list — a deliberate departure from this CLI's usual
list-field convention (e.g. campaigns update --negative-keywords replaces
the entire array in one call). Мастер кампаний has no API, variant sets can
be large, and forcing every variant to be re-typed just to fix one typo would
defeat the point of a partial update — see
direct_cli/browser/masters.py::_set_repeating_value for the full rationale.
Writing to a slot that is currently empty is refused (UsageError) — this
only edits variants that already exist, it does not add new ones. An empty or
whitespace-only replacement (--headline "1=") is refused for the mirror-image
reason: blanking a slot would delete a live ad variant, not replace it — use
--clear-headline N/--clear-text N (repeatable, 1-based slot number) to
actually delete a variant instead, which clicks that slot's own clear button.
A slot number cannot be passed to both a set flag and its clear counterpart
in the same call, and clearing an already-empty slot is refused too. Adding a
brand-new variant beyond the page's fixed slot count is not supported —
Yandex's edit page has no "add another" control. Editing variant weights
isn't supported either, but for a different reason: Мастер кампаний has no
weight/priority UI for these slots at all (confirmed live), unlike ordinary
text-campaign ad variants.
--image "N=/path/to/file.png" replaces the image currently at position N of
the campaign's image set with a local PNG/JPEG/GIF file, through the edit
page's image manager modal. Known limitation — the set is reordered.
Yandex has no "replace this slot" primitive for images at all, only "remove
from the set" and "add to the set", so a point replacement is composed from
the two — and a newly uploaded image is always appended to the END of the set
(confirmed live), never inserted at the freed position. Replacing position 2
of [A, B, C, D] therefore yields [A, C, D, NEW], not [A, NEW, C, D].
The flag's semantics are "replace the image that is currently at position N"
(which one to drop), not "put the new image at position N". This has no
effect on ad delivery — Yandex rotates images by performance regardless of
their order in the set. Unlike headlines/texts there is no fixed slot count:
the upper bound for N is whatever the campaign actually has (up to Yandex's
cap of 5), read fresh from the page, and a campaign with no images at all is
a legitimate state that --image refuses with its own explicit error rather
than a generic "out of range". Nonexistent paths and extensions Yandex won't
accept are rejected before any browser opens. --image only replaces an
existing image one at a time — for adding beyond the current set, deleting
without a replacement, or replacing the whole set at once, use masters adimages below. Because both the removal and the upload happen inside the
same open modal, any failure before Save leaves the campaign's saved image
set untouched.
masters adimages get/add/delete/set is the full CRUD counterpart to
--image, mirroring direct adimages get/add/delete's vocabulary (the
API-side ad-image group) for a campaign's whole image set. Unlike --image,
an empty image set is a completely normal state on both ends here — a
campaign can start with zero images (adimages add works from an empty
set), and every image can be deleted (adimages delete --all), exactly like
ad images on a text ad via the API. adimages get is read-only and never
saves. adimages add --image-file PATH (repeatable) appends files, refusing
if the campaign's current count plus the new files would exceed Yandex's cap
of 5. adimages delete removes images addressed by --position (1-based,
as shown by adimages get), --content-id, or --all; --all on an
already-empty set is an idempotent no-op, but naming a specific position or
content ID that doesn't exist is always an error. adimages set --image-file PATH (repeatable) replaces the ENTIRE set — every current image is removed
and every given file uploaded inside one modal session; with no
--image-file at all it would delete every image, so it requires an
explicit --allow-empty to confirm (or use adimages delete --all
instead). All three mutating subcommands accept --launch (same
draft-publishing semantics as masters update) and, like --image, are
NOT idempotent for add/set — a retried call after a partial failure may
upload duplicates.
Later fields (sitelinks, audience, Metrika counters/goals, budget adaptation, video) aren't implemented yet.
A DRAFT campaign's edit page has no "Сохранить кампанию" button at all —
masters update on a DRAFT saves it via "Сохранить как черновик" by
default, keeping it a DRAFT; pass --launch to publish it while saving
instead ("Запустить кампанию"). Has no effect on a non-DRAFT campaign.
Updating many campaigns at once. masters update also takes a whole plan
instead of a single CAMPAIGN_ID, so a batch of campaigns is edited over
one browser session rather than one Chromium launch per campaign (what
actually failed in a live 17-campaign run was the repeated re-entry into
Yandex, not the editing itself):
direct masters update --from-file plan.jsonl
direct masters update --masters-json '[{"CampaignId": 713234064, "Name": "x"}]'{"CampaignId": 713234064, "Headlines": {"1": "Новый заголовок"}, "Texts": {"1": "Текст"}}
{"CampaignId": 713231614, "LandingUrl": "https://lp.example.ru/x", "TrackingParams": "utm_source=direct"}Same convention as keywords add: --from-file (JSONL, one campaign per
line) and --masters-json (the same array inline) are mutually exclusive
with each other and with a positional CAMPAIGN_ID, field flags belong in
the plan rather than alongside it, and batch mode supports only
--format json. Keys are the PascalCase form of this command's own flags,
with 1-based slot numbers and positions — {"Headlines": {"2": "..."}}
is exactly --headline "2=...". An unknown key is an error naming the row,
never a silent skip, and the whole plan (including that local image/video
files exist) is validated before the first save.
A campaign that fails is reported with an Error and does not stop the rest;
the command exits non-zero afterwards. If browser authorization fails mid-plan,
the run stops with a login hint. It never imports cookies or repeats the
operation under another session.
--pacing-ms (default 1000) controls the pause between campaigns,
--moderation-statuses additionally reads each campaign's current moderation
statuses after its save (a snapshot — moderation usually has not run yet at
that point, so a rejection can still appear later), and --dry-run prints
what would be applied without opening a browser.
To import an existing Chrome session instead, run
direct auth login browser --source chrome --chrome-profile "Profile 1".
Chrome profile flags belong to explicit login, not resource commands.
Since this data has no API contract, the parser degrades per-section (a warning, not a hard failure) if Yandex changes the page markup, rather than failing the whole command.
suspend/resume click the campaign overview page's stop/start button and
verify the status actually changed before reporting success — never trusting
the click alone. They're idempotent (already-suspended/-active is a no-op
warning, not an error). There is no --sandbox for these — Мастер
кампаний has no API at all, so there is no isolated test copy; any mutation
hits your real account.
archive is the closest thing to "delete" a non-DRAFT Мастер кампаний has:
a live recon (issue #633) confirmed there is no separate delete action
in Yandex's UI for it, only "Архивировать" on the overview page's own menu.
masters archive clicks that menu item and verifies the campaign actually
shows up as archived in the campaigns grid before reporting success. It is
idempotent (already-archived is a no-op warning) but irreversible from
this CLI — there is no masters unarchive. Same no---sandbox caveat as
suspend/resume above.
delete (issue #782) removes a DRAFT campaign — the one status
archive cannot touch, since a DRAFT's overview page has no "⋮" menu at
all (issue #660). A DRAFT created by mistake (e.g. via masters add --draft) was previously a one-way door: the only documented route out was
launching it, spending real money, purely to gain access to archive. Live
recon found the campaigns grid's own row menu (a separate menu from the
overview page's) does offer "Удалить" for a DRAFT row — masters delete
uses that instead. Refuses anything but DRAFT with a clear error
pointing at masters archive. Unlike every other masters mutation,
Yandex shows no confirmation dialog before deleting — the campaign is
gone the instant the click lands — so this command always asks for its own
confirmation first: interactively by default, or pass --yes to skip the
prompt non-interactively. Same no---sandbox caveat as suspend/resume
above. The campaigns grid is a virtualized SPA (#639/#671): delete
locates the row's DOM node to click its menu, and a row that isn't
currently rendered in the grid's viewport is simply absent from the DOM,
not just off-screen. delete drives the grid's own internal scroll
container programmatically until the target row appears (issue #791,
live-verified against a DRAFT placed outside the initial ~10-row render
window), so an older DRAFT buried under many other campaigns is reached the
same as a just-created one.
add creates a brand-new "Конверсии и трафик" Мастер кампаний by driving the
same create wizard a human uses. It is NOT idempotent — running it twice
with the same arguments creates a second campaign, not an update to the
first, since Мастер кампаний has no API-level duplicate detection the way
campaigns add does. --headline/--text are required (repeat the flag
for multiple values): even though Yandex's own wizard can auto-generate
headlines/texts by scanning the landing page, add refuses to silently
publish AI-written ad copy you never reviewed — pass the text you actually
want published. At least one of --region/--region-id is required
(repeat for multiple regions, and they combine): --region takes Yandex's
exact region wording as free text, while --region-id (issue #652) takes a
numeric RegionId and resolves it to the canonical name via the
GeoRegions dictionary (direct dictionaries get-geo-regions) — use
--region-id to avoid guessing the widget's exact text. Unlike the rest of
masters, resolving --region-id requires valid Yandex Direct API
credentials (same chain as any other command). By default the campaign
launches immediately; pass
--draft to save it as a draft instead (Сохранить как черновик) without
going live. There is no --sandbox for this command either — verify with
--draft first and check the result in the web UI before launching for real.
copy clones an existing Мастер кампаний via the overview page's "⋮" menu →
"Клонировать" — the same action a human uses in the web UI. Yandex pre-fills
the new campaign from the source's headlines, texts, images, budget, and
display region verbatim, which sidesteps the region text-matching issues
add --region/--region-id can hit (issues #652/#656/#657) since nothing is
retyped or re-matched. Nothing on the copy is renamed or edited beyond what
Yandex itself does (it appends " — N" to the name) — this command is
intentionally a 1:1 mirror of the web UI's clone action; use masters update
afterwards for any further changes. Not idempotent, same as add:
running it twice creates a SECOND copy, not an update to the first. By
default the copy is saved as a draft (--draft); pass --launch to launch
it immediately in production instead. Same no---sandbox caveat as add.
Browser authorization: one session file —
~/.direct-cli/playwright/session.json (0600). direct auth login browser
imports the already logged-in Chrome session headlessly (--source chrome
is the default — no window opens). The visible login window is explicit
only: direct auth login browser --source manual waits up to --timeout
seconds (default 300) for the manual login and requires a terminal unless
--allow-no-tty is supplied. The resulting session is saved straight into
that file — no persistent profile is created.
Any browser command copies the live Chrome session into that file
automatically when it is missing, corrupt or expired — headlessly, with no
window opened for the copy (Keychain may be asked once for the
cookie-decryption key). --profile-dir selects the source Chrome
user-data-dir and --chrome-profile the Chrome profile subdirectory; both
require --source chrome.
direct auth status browser inspects the saved session without opening
a browser; it reports saved state, not proof of current server-side validity.
direct auth doctor diagnoses dependencies and saved session metadata;
cookie decryption checks are skipped. A missing or failed import produces
a login hint.
direct auth logout browser deletes the session file and any leftover
~/.direct-cli/chrome-profile/ from older versions. It does not touch the
original Chrome profile. Old browser-auth commands
remain deprecated compatibility aliases; their stdout formats are preserved.
The API has no per-field change journal: changes.checkCampaigns reports
only ChangesIn: SELF|CHILDREN|STAT, changes.check returns bare lists of
changed IDs, and Live v4's GetEventsLog logs system events
(BannerModerated, MoneyOut, …) — none of them can answer "who changed
this field, when, and from what to what". That history exists only in the web
interface's «История изменений» section, so direct history reads it through
the same saved browser session direct masters uses (same no---login
rule):
direct history get direct history get --limit 50 direct history get --campaign-ids 77593206 direct history get --date-from 2026-07-01 --date-to 2026-08-13 direct history get --categories CAMPAIGN_STRATEGY direct history get --logins someone --change-sources WEB
It is a separate group rather than a masters subcommand because the section
is account-wide: records cover ordinary ТГО/ЕПК campaigns, ad groups and
ads just as much as Мастера, and --campaign-ids is only a filter over that.
Every filter is applied server-side, and results paginate automatically.
Each record carries Datetime, Login, ChangeSource, Category,
EventType, CampaignId/CampaignName, plus the event's own fields under
Event. Event is passed through as-is: it is a union with a different
shape per EventType (CampaignStrategyEvent carries the
oldStrategy/newStrategy pair, CampaignStatusChangeEvent carries
neither), so a change that dropped a strategy goal is visible as
Event.oldStrategy.goalId → Event.newStrategy.goalId.
A bare --date-from/--date-to date covers the whole day; pass
YYYY-MM-DDTHH:MM:SS for an exact window. Without them, the period the
section itself defaults to is used.
The API has no support surface either: the chat in the Direct UI is a Yandex
Messenger widget, so direct support reads and writes that same private chat
through the saved browser session (same no---login rule as
direct masters):
direct support list
direct support list --limit 20 --format table
direct support get 1204
direct support send --message "Здравствуйте! Подскажите, как подключить счётчик" --dry-run
direct support rate 5 --dry-runThe chat is one continuous thread without ticket numbers, so conversations
are inferred: one ends when you rate the support's work (the bot asks for a
rating at the end of every request) or the bot closes it for lack of an answer
(«Напишите нам, если вопрос ещё актуален»), and your next message starts the
next one. A low rating is the exception — the bot then asks what went wrong and
the dialog goes on in the same conversation. A request that was neither rated
nor closed ends by silence: you writing again more than two days after the last
support message starts a new conversation. A conversation's id is the
sequence number of its first message. support list returns, newest first,
id, started, updated, status (open/closed), messages and a
preview of what you asked; support get <id> returns the messages (id,
from = me/support, date, time, text, plus media/links for
attachments and buttons for a bot's answer options). Promo mailings from the «Яндекс Директ» account are ordinary
support messages and attach to the conversation they arrive in. Opening the
chat to read it marks its messages as read.
support send types the text into the chat and presses Enter, but only after
reading the input field back and finding exactly that text — a leftover draft
stops it: nothing is sent and the field is left for you to clear. The message
reaches the support team and cannot be recalled, so the command asks for
confirmation (--yes in scripts), never retries, and says so when it cannot
confirm delivery; --dry-run only prints what would be sent. There is no
conversation id to reply to — it is one chat, and writing after a closed
conversation opens a new one. DIRECT_SUPPORT_CHAT_URL overrides the chat URL
should Yandex serve a different support account.
support rate <1-5> answers the bot's «Хотим убедиться, что помогли. Оцените,
пожалуйста, нашу работу.» request by pressing that button in the chat. It only
works while that request is the newest message in the chat (otherwise nothing
is pressed and the command says why). A rating cannot be changed and ends the
conversation; a low rating makes the bot ask what went wrong, and the dialog
goes on — answer it with support send. Like send, it asks for confirmation
(--yes in scripts), never retries, and --dry-run only prints what would be
pressed.
auth login has two explicit domains: direct auth login api (an OAuth
token for API/v4 commands, saved in an auth profile) and
direct auth login browser (a browser session for browser commands:
masters, history, ...). Bare direct auth login is the API login.
The API login is headless by default — terminals included (issue #962): it
prints the API authorization URL (with pending PKCE) and completes later
through --code or --code -. It never asks questions, never opens a
browser and never writes the folder .env; bind the folder explicitly
with direct auth write --profile NAME (pass --copy-token there to
store the raw token instead of a link — an explicit choice, since a raw
copy has no profile identity, issue #917). Saving into an existing profile
(including the default default) requires explicit --force; without it
the save refuses to overwrite stored credentials. The CLI never replaces
stored credentials, profiles or browser sessions silently: a replacement is
either the command's declared purpose (and it reports what was replaced) or
requires explicit --force.
The dialog flow — profile menu, OAuth code prompt, browser window, and a
.env link for this folder — exists only behind explicit
direct auth login api --interactive. It needs a terminal and refuses to
combine with --format json, --start-pkce, --code - or --code-stdin.
A profile created through the menu does not change the global active
profile; a fresh profile name comes from the account login and gets a
numeric suffix when the name is taken. API credentials and browser cookies
stay separate and may belong to different accounts.
direct auth login # = direct auth login api (headless) direct auth login api direct auth login api --interactive # explicit dialog login (needs a terminal) direct auth login browser # headless Chrome import (the default) direct auth login browser --source manual # explicit visible login window direct auth status # both domains direct auth status api direct auth status browser direct auth doctor direct auth logout api --profile agency1 direct auth logout browser
--allow-no-tty only overrides the terminal check of the explicit manual
login (--source manual); it never authorizes browser UI by itself.
Chrome import stays headless, including when verification fails: there is
no automatic headful retry. All resource commands fail with
a helpful error when authorization is missing, including in a terminal.
direct without arguments still shows help. API logout removes the named
local profile and this folder's API binding, preserving other .env settings.
The binding includes local token/login values, secret-manager references and
the folder's global-profile inheritance setting.
It does not revoke token copies in other folders, external environments, or
secret managers. Browser logout removes the saved browser session.
auth status reports both domains (an API: and a Browser: block);
combined JSON has api and browser sections plus the api fields
mirrored at the top level (has_token, login, … — the pre-split flat
contract, #969). auth status api and
auth status browser select one domain. Two-step PKCE/stdin login remains
the scripted path. The old --method/--headful flags (and the domainless
--interactive) stay removed stubs naming the exact replacement;
--headful names --source manual (issue #962).
Create a .env file in your working directory:
YANDEX_DIRECT_TOKEN=your_access_token YANDEX_DIRECT_LOGIN=your_yandex_login
Or pass credentials directly per command:
direct --token YOUR_TOKEN --login YOUR_LOGIN campaigns get
Use profile-specific credentials from .env:
YANDEX_DIRECT_TOKEN_AGENCY1=token-1 YANDEX_DIRECT_LOGIN_AGENCY1=client-login-1 YANDEX_DIRECT_TOKEN_AGENCY2=token-2 YANDEX_DIRECT_LOGIN_AGENCY2=client-login-2
OAuth and profile commands:
direct auth login direct auth login --profile agency1 direct auth login --profile agency1 --format json direct auth login --code abc123 --profile agency1 printf '%s\n' abc123 | direct auth login --code - --profile agency1 direct auth list direct auth use --profile agency1 direct auth status --profile agency1 direct --profile agency1 campaigns get
Notes:
- Legacy profile environment variable is not used.
- Select credentials with
--profile. --loginremains Direct client login.- Authorization is performed via
direct auth login. - OAuth profiles store refresh tokens and refresh access tokens automatically.
- The login is headless everywhere — terminals included (issue #962). Run
direct auth login --profile NAMEfirst (it prints the authorize URL), then finish withdirect auth login --code - --profile NAMEand pass the code on stdin. direct auth login --code CODE --profile NAMEremains supported for compatibility, but automation should use--code -to avoid exposing the code in process arguments.- If the first step includes
--client-secret, the secret is remembered for the matching completion step. - If a profile already stores a confidential OAuth client,
direct auth login --code CODE --profile NAMEreuses the savedclient_idandclient_secret. Re-logging into an existing profile requires--force. direct auth login --oauth-token TOKENis a manual access-token import and does not auto-refresh.- A login never writes the folder
.env. Bind the folder withdirect auth write --profile NAME— it links the folder to the profile (aYANDEX_DIRECT_BIND_PROFILEline in the folder's.env, no secret on disk;--copy-tokenstores the raw credentials — the explicit choice the dialog menu also offers). The dialog flow itself requiresdirect auth login api --interactive. - Alias
auth_loginis not supported.
Credential resolution priority:
| Priority | Source | Example |
|---|---|---|
| 1 | Explicit CLI options | direct --token TOKEN --login LOGIN campaigns get |
| 2 | Explicit profile credentials | direct --profile agency1 campaigns get |
| 3 | Base env vars or current working directory .env |
YANDEX_DIRECT_TOKEN, YANDEX_DIRECT_LOGIN |
| 4 | Folder profile link (issue #917) | YANDEX_DIRECT_BIND_PROFILE=agency1 in cwd .env |
| 5 | Active profile credentials | direct auth use --profile agency1 |
| 6 | 1Password references | --op-token-ref, YANDEX_DIRECT_OP_TOKEN_REF |
| 7 | Bitwarden references | --bw-token-ref, YANDEX_DIRECT_BW_TOKEN_REF |
Direct CLI automatically loads only the .env file from the current working
directory, i.e. the directory where you run direct. It does not search for
.env from the installed package or source-code location. Without an explicit
--profile, base YANDEX_DIRECT_TOKEN / YANDEX_DIRECT_LOGIN from env or cwd
.env win over the folder link and the active OAuth profile; the link
resolves its named profile like an explicit --profile and fails loudly when
that profile is gone (issue #917). With explicit --profile, Direct CLI
uses only that profile's OAuth/profile-env credentials and does not fall back to
base YANDEX_DIRECT_LOGIN; this prevents mixing accounts. direct auth status
without --profile reports the effective credential source; with --profile it
reports that profile.
An unbound folder has no credentials (#904). When credentials would come
from the active profile fallback (priority 5) — e.g. a brand-new folder with
no .env — the CLI no longer inherits them silently:
- Non-interactive sessions (scripts, sub-processes): API commands fail with
UsageErrorand binding hints until the folder is bound. Bind a folder without prompts viadirect auth write --profile <name>— it links the folder to the profile (aYANDEX_DIRECT_BIND_PROFILEline in the cwd.env, merge,0600, no secret on disk;--copy-tokenstores the raw credentials instead);--profileis required, nothing is bound implicitly. auth logout api --profile Aremoves the folder link only when it names A: a link to another profile, a raw token copy and the inherit flag are kept and reported (issue #917).- Terminal sessions follow the same rule: an ordinary command never opens a
login dialog. Run
direct auth login apito authorize a profile, then bind the folder withdirect auth write --profile <name>(or usedirect auth login api --interactiveto authorize and bind through the dialog menu). A profile created through the menu does not change the global active profile. direct auth statusin an unbound folder reportssource=null/has_token=falseand notes the global profile (global_profile/global_loginin JSON) without presenting it as this folder's credentials.--dry-runneeds no credentials and stays silent;--sandboxdoes not bypass the gate — the sandbox authenticates under the same profile login.- Set
YANDEX_DIRECT_INHERIT_GLOBAL=1to restore the former silent inheritance for unattended workflows (auth status then labels itsource=global-profile-inherited).
Optional output — data you did not ask for — is hidden by default and printed to stderr only, so stdout stays byte-identical for pipes and JSON parsing:
| Env var | Effect |
|---|---|
DIRECT_SHOW_UNITS=1 |
After each successful v5 API call, one stderr line with the API Units points: API units: <spent>/<rest>/<daily limit> (e.g. API units: 10/20828/64000), plus (used-login: <login>) for agency accounts (Units-Used-Login). The v4 Live API returns no Units headers, so v4 commands (direct balance, …) stay silent under this switch |
DIRECT_QUIET=0 |
Master switch back to the legacy behaviour for optional output that existed before #925 (Units had none) |
DIRECT_SHOW_UNITS=1 works even with DIRECT_QUIET=1; an explicit
DIRECT_SHOW_UNITS=0 wins over DIRECT_QUIET=0. Safety output — inherited
credential warnings (#909), sandbox notices, API warnings — is never
suppressed. Limit errors already include Units in their text (#921).
Tests follow the safe credential order. Live-API test suites (e.g.
tests/test_v4_live_contracts.py) readYANDEX_DIRECT_TOKEN/YANDEX_DIRECT_LOGINfrom the environment first, only then fall back to the activedirect authprofile, and skip the test if neither is set. This prevents a developer machine with an active profile from silently hitting production on a plainpytestinvocation. SeeCLAUDE.mdfor the contract.
Install with pip install direct-cli, then run commands with direct.
Invoking the deprecated direct-cli entrypoint exits with
use direct instead of direct-cli.
Yandex removed the legacy v4 GetBalance method. Direct CLI uses the v4 Live
AccountManagement method with Action=Get for direct balance, returning
money fields such as Amount, AmountAvailableForTransfer, and Currency.
direct balance direct balance --logins client-login,other-client --format table direct balance --logins client-login --dry-run
| Option | Description |
|---|---|
--token |
API access token |
--login |
Direct client login |
--profile |
Credential profile name |
--sandbox |
Use sandbox API |
--locale |
Help/message language: ru (default) or en. Also set via YANDEX_DIRECT_CLI_LOCALE. Command and flag names are unchanged. |
Help text is shown in Russian by default. Switch to English with
direct --locale en ... or export YANDEX_DIRECT_CLI_LOCALE=en. For example,
direct v4finance --help shows the financial master-token setup steps using
the locale-appropriate Yandex Direct UI labels.
direct v4goals get-stat-goals --campaign-ids 123,456 direct v4goals get-retargeting-goals --campaign-ids 123,456 --format table direct v4goals get-stat-goals --campaign-ids 123 --dry-run
Campaign tags are managed as {TagID, Tag} pairs. Use TagID=0 to create a
new campaign tag. Banner/ad tags are assigned by campaign tag IDs. Update
methods replace the full tag list for the target campaign or banner, so pass
existing tags again if they must remain assigned. Ad group tags are filter-only
through direct adgroups get --tag-ids/--tags; this release does not add ad
group tag mutation commands.
direct v4tags get-campaigns --campaign-ids 3193279,1634563 direct v4tags get-banners --banner-ids 2571700,2571745 direct v4tags get-banners --campaign-ids 3193279 direct v4tags update-campaigns --campaign-id 3193279 --tag 0=akapulko --tag 16590=orange --dry-run direct v4tags update-banners --banner-ids 2571700,2571745 --tag-ids 16590,16734 --dry-run direct v4tags update-banners --banner-ids 2571700 --clear-tags --dry-run
direct v4events get-events-log --from 2026-04-14T00:00:00 --to 2026-04-15T00:00:00 direct v4events get-events-log --from 2026-04-14T00:00:00 --to 2026-04-15T00:00:00 --currency RUB --limit 100 --offset 0 --format table direct v4events get-events-log --from 2026-04-14T00:00:00 --to 2026-04-15T00:00:00 --last-event-only Yes --with-text-description Yes --filter-campaign-ids 123,456 --filter-event-type MoneyOut,MoneyIn
Wordstat reports are asynchronous. Direct CLI makes exactly one API call per
command and does not poll automatically; repeat list-reports or get-report
yourself until the report is ready.
direct v4wordstat create-report --phrases "buy laptop,buy desktop" --geo-ids 213
direct v4wordstat list-reports --format table
direct v4wordstat get-report --report-id 123 --format table
direct v4wordstat delete-report --report-id 123get-suggestion returns up to 20 related phrases for the seed phrases. Repeat
--keyword for multiple seeds. The method consumes API points.
direct v4keywords get-suggestion --keyword холодильник --keyword камера
direct v4keywords get-suggestion --keyword "buy laptop" --format table
direct v4keywords get-suggestion --keyword холодильник --dry-runAdImageAssociation is exposed as two typed commands. get reads ad-to-image
associations via an optional selection filter (an empty filter returns up to
10000 associations). set attaches or detaches images: --association AD_ID=HASH
attaches an image, --association AD_ID (no hash) detaches the current image
(max 10000 associations per call).
direct v4adimage get --ad-ids 123,456 --status-moderate Yes --limit 20
direct v4adimage get --campaign-ids 789 --format table
direct v4adimage set --association 123=abc123hash --association 456 --dry-runBudget forecasts are asynchronous. Direct CLI makes exactly one API call per
command and does not poll automatically; repeat list or get yourself until
the forecast is ready.
direct v4forecast create --phrases "buy laptop,buy desktop" --geo-ids 213 --currency RUB direct v4forecast create --phrases "buy laptop" --geo-ids 213 --auction-bids Yes --common-minus-words "used,broken" direct v4forecast list --format table direct v4forecast get --forecast-id 123 --format table direct v4forecast delete --forecast-id 123
Finance methods require an extra financial token for money operations. In the
Yandex Direct web UI, open Tools -> API -> Financial operations, enable the
financial operations checkbox, click Save, then issue the master token on the
same Financial operations page and confirm by SMS. Direct CLI can compute the
per-request token from --master-token, --operation-num, and
--finance-login; alternatively pass a precomputed token with --finance-token.
Environment variables are
YANDEX_DIRECT_MASTER_TOKEN, YANDEX_DIRECT_FINANCE_LOGIN,
YANDEX_DIRECT_FINANCE_TOKEN, and YANDEX_DIRECT_OPERATION_NUM.
transfer-money, pay-campaigns, and pay-campaigns-by-card are dry-run-only
in this release and always require --dry-run; create-invoice can be sent
live when --dry-run is omitted. Dry-run output masks the financial token.
pay-campaigns-by-card has an undocumented request shape; its dry-run body
mirrors the documented pay-campaigns shape as a best-effort preview.
⚠ Finance commands have not been tested against the live API. Treat the request shapes as best-effort and always verify with
--dry-runbefore sending anything that runs live (create-invoice).
direct v4finance get-clients-units --logins client-login,other-client --format table direct v4finance get-credit-limits --master-token MASTER_TOKEN --operation-num 123 --finance-login agency-login direct v4finance create-invoice --payment 123=100.50 --payment 456=25 --currency RUB --master-token MASTER_TOKEN --operation-num 124 --finance-login agency-login --dry-run direct v4finance check-payment --custom-transaction-id A123456789012345678901234567890B direct v4finance transfer-money --from-campaign-id 123 --to-campaign-id 456 --amount 100.50 --currency RUB --master-token MASTER_TOKEN --operation-num 123 --finance-login agency-login --dry-run direct v4finance pay-campaigns --campaign-ids 123,456 --amount 100.50 --contract-id CONTRACT_ID --pay-method Bank --currency RUB --master-token MASTER_TOKEN --operation-num 123 --finance-login agency-login --dry-run direct v4finance pay-campaigns-by-card --campaign-ids 123,456 --amount 100.50 --currency RUB --master-token MASTER_TOKEN --operation-num 128 --finance-login agency-login --dry-run
EnableSharedAccount accepts one client Login (agencies only).
AccountManagement exposes the five official v4 Live actions:
Get, Update, Deposit, Invoice, and TransferMoney. Get is
read-only and runs against production without --dry-run. Update,
Deposit, Invoice, and TransferMoney are mutations: they require
--dry-run in production and can be sent live only with top-level
--sandbox. Deposit, Invoice, and TransferMoney are financial
operations that need --finance-token (or --master-token +
--operation-num + --finance-login); dry-run output masks the
financial token.
direct v4account enable-shared-account --client-login client-login --dry-run direct v4account account-management --action Get direct v4account account-management --action Get --logins client-a,client-b direct v4account account-management --action Get --account-ids 1327944,1327945 direct v4account account-management --action Update --account-id 1327944 --day-budget 100.50 --spend-mode Default --money-in-sms Yes --money-out-sms No --email [email protected] --money-warning-value 25 --dry-run direct v4account account-management --action Deposit --payment 1327944=100.50 --currency RUB --master-token MASTER_TOKEN --operation-num 124 --finance-login agency-login --dry-run direct v4account account-management --action Deposit --payment 1327944=100.50 --currency RUB --origin Overdraft --contract CONTRACT_ID --master-token MASTER_TOKEN --operation-num 125 --finance-login agency-login --dry-run direct v4account account-management --action Invoice --payment 1327944=100.50 --currency RUB --master-token MASTER_TOKEN --operation-num 126 --finance-login agency-login --dry-run direct v4account account-management --action TransferMoney --from-account-id 1327944 --to-account-id 1327945 --amount 50.00 --currency RUB --master-token MASTER_TOKEN --operation-num 127 --finance-login agency-login --dry-run direct --sandbox v4account enable-shared-account --client-login client-login
Some v4 Live methods present in the API registry are intentionally not exposed as CLI commands:
DeleteReport/DeleteOfflineReport— disabled by Yandex (the official docs list them under "Отключенные методы" / "Метод отключен. Используйте API версии 5"). Use the v5 reports API instead.PingAPI,PingAPI_X,GetVersion,GetAvailableVersions— service diagnostics / version probes with no documented request shape; not useful as user-facing CLI commands.PayCampaignsByCardis exposed but dry-run-only (undocumented, financially sensitive — see V4 Live Finance above).
The current CLI convention is defined as follows.
The canonical command shape is:
direct <group> <command> [flags]
Naming rules:
-
group:- lowercase ASCII only
- no underscores
- multiword groups are concatenated
- examples:
dynamicads,smartadtargets,negativekeywordsharedsets
-
command:- lowercase only
- multiword commands use kebab-case
- examples:
get,set-bids,check-campaigns,has-search-volume
The direct executable defines the public naming contract. The
direct-cli package name and deprecated shim do not define canonical CLI
names. tapi-yandex-direct may influence the internal transport layer, but it
does not define canonical CLI names.
The current policy is canonical-only. Historical aliases are not preserved in the runtime CLI by default. If compatibility is ever needed, an alias must be added as an explicit exception with the concrete legacy syntax that still has to be supported.
| Legacy name | Canonical name |
|---|---|
dynamictargets |
dynamicads |
smarttargets |
smartadtargets |
negativekeywords |
negativekeywordsharedsets |
list |
get |
checkcamp |
check-campaigns |
checkdict |
check-dictionaries |
- All user-facing input must be passed only through typed CLI flags.
--jsonis not part of the public CLI contract.- User-facing parameters must not be passed through
--json. - The CLI must not accept
SelectionCriteria, nested payloads, update payloads, bidding rules, or any other user-facing command input through--json. - Typed flags and JSON blobs must not be mixed as part of one public command contract.
- If the API requires a complex object, the CLI must expose explicit flags or subcommands instead of forwarding raw JSON.
- Every canonical CLI command must be written strictly on a single line.
- Multi-line command formatting is not allowed.
- Shell line continuation using
\is forbidden in canonical documentation, help text, tests, and examples.
Allowed:
direct dictionaries get-geo-regions --region-ids 225,187 --fields GeoRegionId,GeoRegionName
Not allowed: splitting a canonical direct ... command over multiple shell
lines with \.
- List inputs use comma-separated CLI syntax where appropriate.
- Money and bid values are passed only in micro-rubles, exactly as Yandex Direct API long fields define them. The CLI does not accept decimal currency amounts or convert currency units; values below 100,000 trigger a validation hint suggesting the correct scale.
- Selector fields remain explicit flags, for example:
--id--campaign-id--adgroup-id
- Nested API structures must be projected into typed flags instead of blob JSON.
- Help text must not advertise JSON as an alternative input path.
- Changes timestamps must include an explicit timezone:
YYYY-MM-DDTHH:MM:SSZor an offset such asYYYY-MM-DDTHH:MM:SS+03:00. - Other datetime parameters use their method-specific documented format.
- Datetime values must be passed as a single shell token.
- Canonical
changesexamples should use theZsuffix; explicit offsets are accepted and normalized to UTC. - Canonical examples must not use quoted space-separated datetime values.
Use:
direct changes check-campaigns --timestamp 2026-04-14T00:00:00Z
Do not use: a changes timestamp without a timezone suffix, or a quoted
timestamp that contains a space between the date and time.
READMEmust use only canonical syntax.READMEmust use only single-line command examples.- Canonical examples must not contain
--json. - Help output and tests must enforce the same contract.
Valid canonical examples:
direct campaigns get --ids 1,2,3
direct changes check-campaigns --timestamp 2026-04-14T00:00:00Z
direct keywordsresearch has-search-volume --keywords "buy laptop,buy desktop"
direct smartadtargets update --id 456 --priority HIGH
direct dynamicads set-bids --id 789 --bid 12500000 --context-bid 9000000 --priority HIGH
direct dictionaries get-geo-regions --name Moscow --region-ids 225,187 --exact-names Москва,Санкт-Петербург --fields GeoRegionId,GeoRegionNameInvalid examples include command lines that pass raw JSON flags, use shell
line continuations, omit the timezone suffix from changes datetimes, or quote
space-separated datetime values.
# Get campaigns
direct campaigns get
direct campaigns get --status ACTIVE
direct campaigns get --ids 1,2,3 --format table
direct campaigns get --fetch-all --format csv --output campaigns.csvReading the weekly budget limit (WeeklySpendLimit). WeeklySpendLimit
is not a valid root FieldNames value (CampaignFieldEnum in the WSDL) —
passing it via --fields fails with API err 8000 "An item in the FieldNames
array contains an invalid enumeration value" (issue #946); it is
intentionally absent from the CLI default fields for the same reason. The
weekly limit lives in the type-specific strategy block: request
BiddingStrategy for the campaign type and read
BiddingStrategy.Search/Network.<Strategy>.WeeklySpendLimit:
direct campaigns get --ids 107707079 --fields Id,Name,Type --text-campaign-field-names BiddingStrategy
The same works via --unified-campaign-field-names,
--dynamic-text-campaign-field-names, --mobile-app-campaign-field-names,
--cpm-banner-campaign-field-names and --smart-campaign-field-names. That
the live API populates the value inside BiddingStrategy is declared by the
WSDL GET-side Strategy* structs but has not been live-verified. Мастер
кампаний budgets are not exposed by campaigns get at all — use
masters list.
Automatic ID batching. campaigns get splits --ids lists longer than
CAMPAIGNS_GET_IDS_BATCH_LIMIT (10; the cap observed through the
yandex-direct-mcp wrapper, issue #946 — the Direct API itself accepted
larger lists in a 2026-06-17 measurement) into chunks, sends them
sequentially and concatenates the results in --ids order; the output
format is unchanged. With --dry-run every chunk request is printed as a
JSON array. --limit applies to the concatenated result, not per chunk.
# Create (use --dry-run to preview the request) direct campaigns add --name "My Campaign" --start-date 2024-02-01 --type TEXT_CAMPAIGN --budget 1000000000 --setting ADD_METRICA_TAG=YES --search-strategy HIGHEST_POSITION --network-strategy SERVING_OFF --dry-run direct campaigns add --name "Dynamic Campaign" --start-date 2024-02-01 --type DYNAMIC_TEXT_CAMPAIGN --setting ADD_METRICA_TAG=NO --search-strategy HIGHEST_POSITION --network-strategy SERVING_OFF --dry-run direct campaigns add --name "Smart Campaign" --start-date 2024-02-01 --type SMART_CAMPAIGN --network-strategy AVERAGE_CPC_PER_FILTER --filter-average-cpc 1000000 --counter-id 123 --dry-run # CPA strategy (single goal): --goal-id required, --average-cpa / --bid-ceiling are micro-rubles direct campaigns add --name "CPA Campaign" --start-date 2026-06-01 --type TEXT_CAMPAIGN --search-strategy AVERAGE_CPA --network-strategy SERVING_OFF --goal-id 1234567 --average-cpa 500000000 --bid-ceiling 1000000000 --counter-ids 111,222 --dry-run # Multi-goal CPA via PriorityGoals (goal_id:value pairs, WSDL PriorityGoalsItem) direct campaigns add --name "Multi-Goal CPA" --start-date 2026-06-01 --type TEXT_CAMPAIGN --search-strategy AVERAGE_CPA_MULTIPLE_GOALS --network-strategy SERVING_OFF --priority-goals 1234567:80000000,9876543:20000000 --bid-ceiling 1000000000 --dry-run # TextCampaign/UnifiedCampaign/DynamicTextCampaign/SmartCampaign/MobileAppCampaign/CpmBannerCampaign optional controls direct campaigns add --name "Text Controls" --start-date 2026-06-01 --type TEXT_CAMPAIGN --counter-ids 111,222 --relevant-keywords-budget-percent 40 --relevant-keywords-mode OPTIMAL --attribution-model AUTO --negative-keyword-shared-set-ids 10,11 --dry-run direct campaigns update --id 12345 --type TEXT_CAMPAIGN --setting ADD_METRICA_TAG=NO --priority-goals 1234567:80000000:YES --tracking-params "utm_source=direct" --dry-run direct campaigns add --name "Package Text" --start-date 2026-06-01 --type TEXT_CAMPAIGN --package-strategy-id 700 --package-platform-search-result YES --package-platform-product-gallery YES --package-platform-network NO --dry-run direct campaigns add --name "Unified Controls" --start-date 2026-06-01 --type UNIFIED_CAMPAIGN --setting ADD_METRICA_TAG=YES --counter-ids 111,222 --tracking-params "utm_source=direct" --attribution-model AUTO --negative-keyword-shared-set-ids 10,11 --dry-run direct campaigns add --name "Unified Package" --start-date 2026-06-01 --type UNIFIED_CAMPAIGN --package-strategy-id 700 --package-platform-search-result YES --package-platform-product-gallery YES --package-platform-maps NO --package-platform-search-organization-list YES --package-platform-network YES --dry-run direct campaigns add --name "Dynamic Controls" --start-date 2026-06-01 --type DYNAMIC_TEXT_CAMPAIGN --setting ADD_METRICA_TAG=YES --dynamic-placement-search-results YES --dynamic-placement-product-gallery NO --counter-ids 111,222 --tracking-params "utm_source=direct" --attribution-model AUTO --negative-keyword-shared-set-ids 10,11 --dry-run direct campaigns update --id 12345 --type DYNAMIC_TEXT_CAMPAIGN --setting ADD_METRICA_TAG=NO --dynamic-placement-search-results NO --priority-goals 1234567:80000000:YES --tracking-params "utm_source=direct" --dry-run direct campaigns add --name "Dynamic Package" --start-date 2026-06-01 --type DYNAMIC_TEXT_CAMPAIGN --package-strategy-id 700 --dry-run direct campaigns add --name "Smart Controls" --start-date 2026-06-01 --type SMART_CAMPAIGN --counter-id 123 --filter-average-cpc 1000000 --setting ADD_TO_FAVORITES=YES --tracking-params "utm_source=direct" --attribution-model AUTO --dry-run direct campaigns add --name "Smart Package" --start-date 2026-06-01 --type SMART_CAMPAIGN --counter-id 123 --package-strategy-id 700 --package-platform-search YES --package-platform-network NO --dry-run direct campaigns add --name "Mobile App Controls" --start-date 2026-06-01 --type MOBILE_APP_CAMPAIGN --setting ADD_TO_FAVORITES=YES --negative-keyword-shared-set-ids 10,11 --dry-run direct campaigns add --name "CPM Banner Controls" --start-date 2026-06-01 --type CPM_BANNER_CAMPAIGN --setting ADD_METRICA_TAG=YES --counter-ids 111,222 --frequency-cap-impressions 5 --frequency-cap-period-days 7 --video-target VIEWS --dry-run direct campaigns add --name "CPM Banner Strategy" --start-date 2026-06-01 --type CPM_BANNER_CAMPAIGN --network-strategy WB_MAXIMUM_IMPRESSIONS --average-cpm 120000000 --strategy-spend-limit 1000000000 --dry-run direct campaigns update --id 12345 --type CPM_BANNER_CAMPAIGN --frequency-cap-impressions 5 --frequency-cap-period-all --dry-run # Notification (Sms/Email) and TimeTargeting via typed CLI flags direct campaigns add --name "Notify+Schedule" --start-date 2026-06-01 --type TEXT_CAMPAIGN --search-strategy HIGHEST_POSITION --network-strategy SERVING_OFF --notification-email [email protected] --notification-send-warnings YES --time-targeting-schedule 1A0123456789ABCDEFGHIJKL --consider-working-weekends YES --dry-run # TrackingParams (campaign subtype UTM / tracking query string) direct campaigns add --name "UTM" --start-date 2026-06-01 --type TEXT_CAMPAIGN --tracking-params "utm_source=direct&utm_campaign={campaign_id}" --dry-run # Update / lifecycle direct campaigns update --id 12345 --name "New Name" --status SUSPENDED --budget 100000000 --start-date 2024-02-10 --end-date 2024-03-01 direct campaigns update --id 12345 --type TEXT_CAMPAIGN --tracking-params "utm_source=direct&utm_medium=cpc" --dry-run direct campaigns suspend --id 12345 direct campaigns resume --id 12345 direct campaigns archive --id 12345 direct campaigns unarchive --id 12345 direct campaigns delete --id 12345
direct adgroups get --campaign-ids 1,2,3 --limit 50 direct adgroups add --name "Group 1" --campaign-id 12345 --region-ids 1,225 --negative-keywords "repair,used" --tracking-params "utm_source=direct" --dry-run direct adgroups add --name "Text Feed Group" --campaign-id 12345 --region-ids 1,225 --feed-id 170 --feed-category-ids 10,11 --dry-run direct adgroups add --name "Dynamic Group" --campaign-id 12345 --type DYNAMIC_TEXT_AD_GROUP --region-ids 1,225 --domain-url example.com --autotargeting-category EXACT=YES --dry-run direct adgroups add --name "Dynamic Feed Group" --campaign-id 12345 --type DYNAMIC_TEXT_FEED_AD_GROUP --region-ids 1,225 --feed-id 170 --autotargeting-category EXACT=YES --dry-run direct adgroups add --name "CPM Keywords Group" --campaign-id 12345 --type CPM_BANNER_KEYWORDS_AD_GROUP --region-ids 1,225 --dry-run direct adgroups add --name "CPM User Profile Group" --campaign-id 12345 --type CPM_BANNER_USER_PROFILE_AD_GROUP --region-ids 1,225 --dry-run direct adgroups add --name "CPM Video Group" --campaign-id 12345 --type CPM_VIDEO_AD_GROUP --region-ids 1,225 --dry-run direct adgroups add --name "Smart Group" --campaign-id 12345 --type SMART_AD_GROUP --region-ids 1,225 --feed-id 170 --ad-title-source FEED_NAME --ad-body-source FEED_NAME --dry-run direct adgroups add --name "Unified Group" --campaign-id 12345 --type UNIFIED_AD_GROUP --region-ids 1,225 --offer-retargeting YES --dry-run direct adgroups add --name "Mobile App Group" --campaign-id 12345 --type MOBILE_APP_AD_GROUP --region-ids 1,225 --store-url https://apps.apple.com/app/id123456789 --target-device-types DEVICE_TYPE_MOBILE,DEVICE_TYPE_TABLET --target-carrier WI_FI_AND_CELLULAR --target-operating-system-version 14.0 --dry-run direct adgroups update --id 67890 --negative-keyword-shared-set-ids 10,11 --tracking-params "utm_source=direct" direct adgroups update --id 67890 --feed-id 170 --feed-category-ids 10,11 direct adgroups update --id 67890 --domain-url example.com --autotargeting-settings-exact YES --autotargeting-settings-without-brands YES --dry-run direct adgroups update --id 67890 --dynamic-feed --autotargeting-category EXACT=YES --dry-run direct adgroups update --id 67890 --target-device-types DEVICE_TYPE_TABLET --target-carrier WI_FI_ONLY --target-operating-system-version 13.0 direct adgroups update --id 67890 --ad-title-source FEED_NAME --ad-body-source FEED_DESCRIPTION direct adgroups update --id 67890 --offer-retargeting NO direct adgroups suspend --id 67890 direct adgroups resume --id 67890 direct adgroups delete --id 67890
adgroups suspend/adgroups resume are not 1:1 WSDL mirrors: the AdGroups
service has no suspend/resume method (only add/get/update/delete).
They emulate "pause the group" the way the web UI and Direct Commander do —
by resolving the group's ads via ads.get and suspending/resuming them via
ads.suspend/ads.resume (issue #573). Two consequences:
resumecannot tell which ads it paused apart from ads a human suspended by hand earlier — it resumes every ad currently in the group, so a mixed-status group loses any manual suspensions.- An empty group (or one whose ads no longer exist) prints an empty
SuspendResults/ResumeResultswith no request sent.
direct ads get --campaign-ids 1,2,3 direct ads get --adgroup-ids 45678 --format table direct ads add --adgroup-id 12345 --type TEXT_AD --title "Title" --text "Ad text" --href "https://example.com" --dry-run direct ads add --adgroup-id 12345 --type TEXT_AD --title "Title" --text "Ad text" --href "https://example.com" --title2 "Second headline" --display-url-path "deals" --mobile YES --vcard-id 111 --sitelink-set-id 222 --turbo-page-id 333 --ad-extensions "444,555" --dry-run direct ads add --adgroup-id 12345 --type TEXT_AD --title "Title" --text "Ad text" --href "https://example.com" --final-url "https://final.example.com" --video-extension-creative-id 777 --price-extension-price 123450000 --price-extension-price-qualifier FROM --price-extension-price-currency RUB --business-id 777 --prefer-vcard-over-business NO --erir-ad-description "Text ad object" --dry-run direct ads add --adgroup-id 12345 --type RESPONSIVE_AD --texts "Text one,Text two" --titles "Title one,Title two" --image-hashes hash1,hash2 --video-extension-ids 111,222 --href "https://example.com" --price-extension-price 123450000 --price-extension-price-qualifier FROM --price-extension-price-currency RUB --business-id 777 --erir-ad-description "Responsive ad object" --dry-run direct ads add --adgroup-id 12345 --type SHOPPING_AD --feed-id 170 --default-texts "Default product text" --sitelink-set-id 222 --ad-extensions "333,444" --business-id 777 --feed-filter-condition "CATEGORY:EQUALS_ANY:shoes|boots" --title-sources NAME,BRAND --text-sources DESCRIPTION --dry-run direct ads add --adgroup-id 12345 --type LISTING_AD --feed-id 171 --default-texts "Default listing text" --feed-filter-condition "CATEGORY:EQUALS_ANY:appliances" --title-sources TITLE --text-sources DESCRIPTION --dry-run direct ads add --adgroup-id 12345 --type TEXT_AD_BUILDER_AD --creative-id 123 --href "https://example.com" --turbo-page-id 456 --erir-ad-description "Builder ad object" --dry-run direct ads add --adgroup-id 12345 --type MOBILE_APP_AD_BUILDER_AD --creative-id 123 --tracking-url "https://track.example.com" --erir-ad-description "Mobile builder ad" --dry-run direct ads add --adgroup-id 12345 --type CPM_BANNER_AD_BUILDER_AD --creative-id 123 --href "https://example.com" --tracking-pixels "https://pixel.example.com/a,https://pixel.example.com/b" --dry-run direct ads add --adgroup-id 12345 --type TEXT_IMAGE_AD --image-hash abcdefghijklmnopqrst --href "https://example.com" --turbo-page-id 555 --final-url "https://final.example.com" --erir-ad-description "Image ad object" --dry-run direct ads add --adgroup-id 12345 --type DYNAMIC_TEXT_AD --text "Dynamic ad text" --image-hash abcdefghijklmnopqrst --vcard-id 111 --sitelink-set-id 222 --ad-extensions "333,444" --dry-run direct ads add --adgroup-id 12345 --type MOBILE_APP_AD --title "Install app" --text "App promo text" --action INSTALL --tracking-url "https://track.example.com" --mobile-app-feature PRICE=YES --video-extension-creative-id 777 --erir-ad-description "Mobile app object" --dry-run direct ads add --adgroup-id 12345 --type MOBILE_APP_IMAGE_AD --image-hash abcdefghijklmnopqrst --tracking-url "https://track.example.com" --erir-ad-description "Mobile image ad" --dry-run direct ads add --adgroup-id 12345 --type SMART_AD_BUILDER_AD --logo-extension-hash logoabcdefghijklmnop --dry-run direct ads update --id 99999 --type TEXT_AD --title "New Title" --text "New text" --href "https://example.com" direct ads update --id 99999 --type TEXT_AD --image-hash abcdefghijklmnopqrst direct ads update --id 99999 --type TEXT_AD --clear-image-hash # remove the image (AdImageHash: null; TEXT_AD / DYNAMIC_TEXT_AD / MOBILE_APP_AD only) direct ads update --id 99999 --type TEXT_AD --title2 "New second headline" --vcard-id 222 direct ads update --id 99999 --type TEXT_AD --callouts-add "111,222" --callouts-remove "333" direct ads update --id 99999 --type TEXT_AD --callouts-set "444,555" direct ads update --id 99999 --type TEXT_AD --video-extension-creative-id 777 --price-extension-price 123450000 --price-extension-price-qualifier FROM --price-extension-price-currency RUB direct ads update --id 99999 --type TEXT_AD --final-url "https://final.example.com" --age-label AGE_18 --business-id 777 --prefer-vcard-over-business NO --erir-ad-description "Text ad object" direct ads update --id 99999 --type DYNAMIC_TEXT_AD --text "Updated dynamic text" --callouts-add "111,222" direct ads update --id 99999 --type MOBILE_APP_AD --mobile-app-feature PRICE=YES --mobile-app-feature CUSTOMER_RATING=NO --video-extension-creative-id 777 --erir-ad-description "Mobile app object" direct ads update --id 99999 --type RESPONSIVE_AD --texts "Text one,Text two" --titles "Title one,Title two" --image-hashes hash1,hash2 --video-extension-ids 111,222 --href "https://example.com" --price-extension-price 123450000 --price-extension-price-qualifier FROM --price-extension-price-currency RUB direct ads update --id 99999 --type TEXT_IMAGE_AD --final-url "https://final.example.com" --erir-ad-description "Image ad object" direct ads update --id 99999 --type SHOPPING_AD --sitelink-set-id 222 --callouts-set "444,555" --business-id 777 --feed-filter-condition "CATEGORY:EQUALS_ANY:shoes|boots" --title-sources NAME,BRAND --text-sources DESCRIPTION --default-texts "Default product text" direct ads update --id 99999 --type MOBILE_APP_IMAGE_AD --image-hash abcdefghijklmnopqrst --tracking-url "https://track.example.com" --erir-ad-description "Mobile image ad" direct ads update --id 99999 --type TEXT_AD_BUILDER_AD --creative-id 123 --creative-erir-ad-description "Creative object" --href "https://example.com" --turbo-page-id 456 direct ads update --id 99999 --type SMART_AD_BUILDER_AD --logo-extension-hash logoabcdefghijklmnop --erir-ad-description "Smart builder ad" direct ads update --id 99999 --type CPM_BANNER_AD_BUILDER_AD --creative-id 123 --href "https://example.com" --tracking-pixels "https://pixel.example.com/a,https://pixel.example.com/b" direct ads delete --id 99999
Available TEXT_AD typed flags for ads add / ads update: --title, --text,
--href, --image-hash, --clear-image-hash (update only — sets
AdImageHash: null; TEXT_AD / DYNAMIC_TEXT_AD / MOBILE_APP_AD only, since
TEXT_IMAGE_AD / MOBILE_APP_IMAGE_AD have a non-nillable AdImageHash),
--title2, --display-url-path, --vcard-id,
--sitelink-set-id, --turbo-page-id, --final-url,
--video-extension-creative-id, --price-extension-*, --business-id,
--prefer-vcard-over-business, and --erir-ad-description. For ads add,
TextAd.PriceExtension requires --price-extension-price,
--price-extension-price-qualifier, and --price-extension-price-currency
when any price-extension flag is used. ads update additionally exposes
--callouts-add, --callouts-remove, and --callouts-set for managing the
TextAdUpdateBase.CalloutSetting (ext:AdExtensionSetting) field on an
existing ad — --callouts-set replaces the whole callout list and is mutually
exclusive with the incremental --callouts-add / --callouts-remove pair.
Price-extension values are passed in micro-rubles, matching the Yandex Direct
API long-unit format directly. ads update also supports
--age-label.
--mobile (default NO) and --ad-extensions are ads add-only —
TextAdUpdate does not contain Mobile, and on update ad-extensions are
managed through the --callouts-* flags above. TEXT_IMAGE_AD additionally
accepts --turbo-page-id, --final-url, and --erir-ad-description.
DYNAMIC_TEXT_AD add requires --text and supports --image-hash,
--vcard-id, --sitelink-set-id, and --ad-extensions; update supports
--text, --image-hash, --vcard-id, --sitelink-set-id, and
--callouts-*.
RESPONSIVE_AD ads add uses --texts and --titles as required
comma-separated lists and also requires --href, --business-id, or both.
Optional creation flags include --image-hashes, --video-extension-ids,
--age-label, --display-url-path, --sitelink-set-id, --ad-extensions,
--price-extension-*, and --erir-ad-description.
SHOPPING_AD and LISTING_AD ads add require --feed-id and one
--default-texts value. Optional creation flags include --sitelink-set-id,
--ad-extensions, --business-id, repeatable --feed-filter-condition
(OPERAND:OPERATOR:ARG1|ARG2), --title-sources, and --text-sources.
Non-SMART AdBuilder add subtypes require --creative-id. TEXT_AD_BUILDER_AD,
CPC_VIDEO_AD_BUILDER_AD, CPM_BANNER_AD_BUILDER_AD, and
CPM_VIDEO_AD_BUILDER_AD require --href, --turbo-page-id, or both. Mobile app
builder subtypes use --tracking-url. CPM builder subtypes also support
--tracking-pixels; non-SMART AdBuilder add subtypes support
--erir-ad-description.
MOBILE_APP_AD add requires --title, --text, and --action; optional add
fields include --mobile-app-feature FEATURE=YES|NO,
--video-extension-creative-id, and --erir-ad-description. MOBILE_APP_IMAGE_AD
add requires --image-hash; add/update support --tracking-url and
--erir-ad-description.
RESPONSIVE_AD update supports --texts, --titles, --image-hashes,
--video-extension-ids, --href, --age-label, --display-url-path,
--sitelink-set-id, --callouts-*, --price-extension-*, --business-id,
and --erir-ad-description.
SHOPPING_AD and LISTING_AD update support --sitelink-set-id,
--callouts-*, --business-id, repeatable --feed-filter-condition
(OPERAND:OPERATOR:ARG1|ARG2), --title-sources, --text-sources, and
--default-texts.
MOBILE_APP_IMAGE_AD update supports --image-hash, --tracking-url, and
--erir-ad-description.
SMART_AD_BUILDER_AD add supports --logo-extension-hash.
AdBuilder update subtypes support --creative-id, --creative-erir-ad-description,
--erir-ad-description, and subtype-specific --final-url, --href,
--turbo-page-id, --tracking-url, and --tracking-pixels. SMART_AD_BUILDER_AD
update supports --logo-extension-hash and --erir-ad-description.
direct keywords get --campaign-ids 1,2,3 direct keywords add --adgroup-id 12345 --keyword "buy laptop" --bid 10500000 --context-bid 5250000 --user-param-1 segment-a --user-param-2 segment-b --dry-run direct keywords add --adgroup-id 12345 --keyword "---autotargeting" --autotargeting-search-bid-is-auto YES --priority HIGH --autotargeting-category EXACT=YES --autotargeting-category BROADER=NO --autotargeting-brand-option WITHOUT_BRANDS=YES --dry-run direct keywords add --adgroup-id 12345 --keyword "---autotargeting" --autotargeting-settings-exact YES --autotargeting-settings-narrow NO --autotargeting-settings-without-brands YES --dry-run direct keywords update --id 88888 --keyword "updated keyword text" direct keywords update --id 88888 --autotargeting-category EXACT=YES --autotargeting-category BROADER=NO --autotargeting-brand-option WITHOUT_BRANDS=YES direct keywords update --id 88888 --autotargeting-settings-broader YES --autotargeting-settings-with-competitors-brand NO direct keywords delete --id 88888
Batch keyword upload (CLI auto-chunks to the API limit of 10 per request):
# From a JSONL file (one keyword object per line) direct keywords add --adgroup-id 12345 --from-file keywords.jsonl # Inline JSON array direct keywords add --adgroup-id 12345 --keywords-json '[{"Keyword":"buy laptop"},{"Keyword":"buy desktop"}]'
Example keywords.jsonl:
{"Keyword":"buy laptop","UserParam1":"src=ad1"}
{"Keyword":"buy desktop","UserParam2":"src=ad2"}
{"Keyword":"купить ноутбук","AdGroupId":99999}- Row keys use WSDL CamelCase:
Keyword,AdGroupId,Bid,ContextBid,UserParam1,UserParam2. BidandContextBidare documentedKeywords.addfields, but they are strategy-dependent:Bidis only for manual strategies, andContextBidis only for manual strategies with independent ad-network bid management. For automatic strategies, Yandex ignores these values and returns warning10160, so omit them from JSONL for auto-strategy / ad-network flows.- Autotargeting row fields are intentionally not accepted in batch mode; use single-item typed flags such as
--autotargeting-search-bid-is-auto,--priority,--autotargeting-category,--autotargeting-brand-option, or--autotargeting-settings-*. --adgroup-idprovides the default group ID; rows can override it via per-rowAdGroupId.- Each effective row must resolve
KeywordandAdGroupId; unknown fields are rejected with the row number. - API limit: 10 items per
keywords.addrequest — see Yandex Direct docs. The CLI sends as many chunks as needed and mergesAddResults. - API limit: 200 keywords per ad group. The CLI prints a warning if any
AdGroupIdin the input exceeds it; the API rejects the excess as per-item errors. - Item-level errors from the API do not abort the batch; the merged output includes successes and per-item errors.
- If a chunk fails with a network-level error mid-batch, already-created Ids are printed to stderr (
Partial success before failure) so a retry doesn't duplicate them. --dry-runshows the first chunk's payload plus{chunks, totalItems, chunkSize}.
# Get a report (saved to file) direct reports get --type CAMPAIGN_PERFORMANCE_REPORT --from 2024-01-01 --to 2024-01-31 --name "January Report" --fields "Date,CampaignId,Clicks,Cost" --format csv --output report.csv direct reports get --type CUSTOM_REPORT --from 2024-01-01 --to 2024-01-31 --name "Goals Report" --fields "Date,CampaignId,GoalsRoi" --goals 12345,67890 --attribution-models AUTO --format csv --output goals-report.csv # List available report types direct reports list-types
Available report types: CAMPAIGN_PERFORMANCE_REPORT, ADGROUP_PERFORMANCE_REPORT, AD_PERFORMANCE_REPORT, CRITERIA_PERFORMANCE_REPORT, CUSTOM_REPORT, REACH_AND_FREQUENCY_CAMPAIGN_REPORT, SEARCH_QUERY_PERFORMANCE_REPORT
# Reference dictionaries and changes direct dictionaries get --names Currencies,GeoRegions direct dictionaries get-geo-regions --name Moscow --region-ids 225,187 --exact-names Москва,Санкт-Петербург --fields GeoRegionId,GeoRegionName # Client info direct clients get --fields ClientId,Login,Currency # Changes direct changes check --campaign-ids 1,2,3 --timestamp 2026-04-14T00:00:00Z --fields CampaignIds,AdGroupIds,AdIds,CampaignsStat direct changes check-campaigns --timestamp 2026-04-14T00:00:00Z direct changes check-dictionaries # Keyword research and retargeting direct keywordsresearch has-search-volume --keywords "buy laptop,buy desktop" direct retargeting add --name "List A" --description "High intent users" --type AUDIENCE --rule "ALL:12345:30|67890:7" --dry-run direct retargeting update --id 55 --name "Renamed" --description "Updated note" --rule "ANY:12345:30" --dry-run direct dictionaries get-retargeting-goals --name exist.ru direct dictionaries get-retargeting-goals --ids 19000000660,19000001592
⚠
dictionaries get-retargeting-goalscallsgetRetargetingGoals, an undocumented/internal v5 method (not declared in thedictionariesWSDL). Direct Commander uses it to resolve a site/app into theExternalIdused byRetargetingList(Type=AUDIENCE) -> Rules[].Arguments[].ExternalId. Yandex gives no stability guarantees for it. A--namesearch can match more than one goalType(e.g.HOSTandAPPLICATION) for similar names — pick the entry with theTypeyour use case needs.
retargeting getreturns each list'sScope(a default field):FOR_TARGETS_ONLY/FOR_TARGETS_AND_ADJUSTMENTS/FOR_ADJUSTMENTS_ONLY. There is no--scopeflag: the WSDL declaresScopeonly onRetargetingListGetItem, so the API assigns it server-side. Verified live (#890):retargeting getreturns every documented scope class present in the account — an empty adjustment-class section means the account has no such lists, not server-side filtering.
# Bids and modifiers direct bids get --campaign-ids 123 --fields CampaignId,AdGroupId,KeywordId,Bid direct bids set --keyword-id 123 --bid 15000000 direct bids set --campaign-id 123 --context-bid 9000000 --autotargeting-search-bid-is-auto YES --priority HIGH direct bids set-auto --keyword-id 123 --max-bid 20000000 --position PREMIUMBLOCK --scope SEARCH --dry-run direct keywordbids set --adgroup-id 321 --search-bid 8000000 --network-bid 3000000 --autotargeting-search-bid-is-auto NO --priority NORMAL direct keywordbids set-auto --keyword-id 321 --target-traffic-volume 100 --increase-percent 10 --bid-ceiling 12500000 --dry-run direct bidmodifiers get --campaign-ids 123 --fields Id,CampaignId,AdGroupId,Level,Type direct bidmodifiers add --campaign-id 123 --type DEMOGRAPHICS_ADJUSTMENT --value 150 --gender GENDER_MALE --age AGE_25_34 --dry-run direct bidmodifiers add --campaign-id 123 --type MOBILE_ADJUSTMENT --value 120 --operating-system-type IOS --dry-run # RETARGETING_SEARCH_ADJUSTMENT is a live-only type (#871): the payload is # mirrored, but Yandex currently rejects its creation with 5005 on every # documented retargeting-list class (#890) — works once acceptance is enabled. direct bidmodifiers add --campaign-id 123 --type RETARGETING_SEARCH_ADJUSTMENT --retargeting-condition-id 12345 --value 120 --dry-run direct bidmodifiers set --id 99 --value 130 --dry-run # Canonical multiword groups direct negativekeywordsharedsets update --id 123 --keywords "foo,bar" # audiencetargets get always needs a filter — the API rejects an empty # SelectionCriteria, so there is no whole-account paging. To sweep the account, # run `campaigns get` first, then page audiencetargets get in batches of campaign ids. direct audiencetargets get --campaign-ids 123 --fields Id,AdGroupId,RetargetingListId,State,ContextBid direct audiencetargets add --adgroup-id 100 --retargeting-list-id 200 --bid 12000000 --priority HIGH --dry-run direct audiencetargets set-bids --id 101 --context-bid 7000000 --priority LOW --dry-run direct dynamicads add --adgroup-id 33 --name "Webpage A" --condition "URL:CONTAINS_ANY:test|shop" --condition "PAGE_CONTENT:CONTAINS:baz" --bid 3000000 --context-bid 2000000 --priority HIGH --dry-run direct smartadtargets add --adgroup-id 55 --name "Audience A" --audience ALL_SEGMENTS --condition "CATEGORY_ID:EQUALS:42" --average-cpc 3000000 --average-cpa 4000000 --priority HIGH --available-items-only YES --dry-run direct smartadtargets update --id 456 --priority HIGH direct smartadtargets set-bids --id 456 --average-cpc 10500000 --average-cpa 15000000 --priority HIGH direct dynamicads set-bids --id 789 --bid 12500000 --context-bid 9000000 --priority HIGH # Shared bidding strategies direct strategies get --limit 5 direct strategies add --name "Shared Clicks" --type WbMaximumClicks --weekly-spend-limit 1000000000 --bid-ceiling 30000000 --dry-run direct strategies add --name "Custom Period Clicks" --type WbMaximumClicks --custom-period-spend-limit 1000000000 --custom-period-start-date 2026-06-01 --custom-period-end-date 2026-06-30 --custom-period-auto-continue YES --dry-run direct strategies add --name "Exploration CPA" --type AverageCpa --average-cpa 4000000 --goal-id 123 --minimum-exploration-budget 200000000 --dry-run direct strategies add --name "CRR Goal Values" --type AverageCrr --average-crr 10 --goal-id 123 --priority-goal 123:2000000:YES --dry-run direct strategies update --id 42 --type WbMaximumClicks --weekly-spend-limit 35000000 --dry-run direct strategies update --id 42 --type WbMaximumClicks --custom-period-spend-limit 35000000 --custom-period-start-date 2026-07-01 --custom-period-end-date 2026-07-31 --custom-period-auto-continue NO --dry-run direct strategies update --id 42 --type MaxProfit --minimum-exploration-budget 0 --dry-run direct strategies update --id 42 --priority-goal 123:2000000:YES --dry-run direct strategies archive --id 42 --dry-run # Dynamic feed ad targets direct dynamicfeedadtargets get --adgroup-ids 123 --limit 5 direct dynamicfeedadtargets add --adgroup-id 33 --name "Feed slice A" --condition "CATEGORY:EQUALS:shoes" --bid 5000000 --dry-run direct dynamicfeedadtargets set-bids --id 789 --bid 6500000 --context-bid 4000000 --dry-run # Extensions, assets, feeds, and clients direct sitelinks add --sitelink "Docs|https://example.com/docs|API docs|12345" --sitelink "Help|https://example.com/help|Desk" --dry-run direct vcards add --campaign-id 555 --country "Russia" --city "Moscow" --company-name "Acme" --work-time 1#5#9#0#18#0 --phone-country-code +7 --phone-city-code 495 --phone-number 1234567 --instant-messenger-client telegram --instant-messenger-login acme_support --point-on-map-x 37.6173 --point-on-map-y 55.7558 --point-on-map-x1 37.60 --point-on-map-y1 55.74 --point-on-map-x2 37.63 --point-on-map-y2 55.77 --dry-run direct adextensions add --callout-text "Free shipping" --dry-run direct adimages add --name banner.png --image-data BASE64DATA --type ICON --dry-run direct creatives add --video-id video-id --dry-run direct feeds add --name "Feed A" --url "https://example.com/feed.xml" --business-type RETAIL --remove-utm-tags YES --feed-login feedbot --dry-run direct feeds add --name "Feed File" --file-feed-path ./feed.xml --business-type RETAIL --dry-run direct feeds update --id 18 --name "Feed A v2" --url "https://example.com/feed-v2.xml" --remove-utm-tags NO --clear-feed-login --clear-feed-password --dry-run direct feeds update --id 18 --file-feed-path ./feed-v2.xml --file-feed-filename feed-v2.xml --dry-run direct clients update --client-info "Priority client" --phone +70000000000 --notification-email [email protected] --notification-lang EN --email-subscription RECEIVE_RECOMMENDATIONS=YES --setting DISPLAY_STORE_RATING=NO --dry-run direct clients update --erir-organization-name "Advertiser LLC" --erir-organization-kpp 770101001 --erir-organization-epay-number epay123 --erir-organization-reg-number 1027700132195 --erir-organization-oksm-number 643 --erir-organization-okved-code 62.01 --dry-run direct clients update --erir-contract-number C-2026-01 --erir-contract-date 2026-01-15 --erir-contract-type CONTRACT --erir-contract-action-type COMMERCIAL --erir-contract-subject-type REPRESENTATION --erir-contract-is-agency-payment NO --erir-contract-price-amount 120000.5 --erir-contract-price-including-vat YES --dry-run direct clients update --erir-contragent-name "Counterparty LLC" --erir-contragent-kpp 770201001 --erir-contragent-phone +70000000001 --erir-contragent-epay-number epay456 --erir-contragent-reg-number 1027700132196 --erir-contragent-oksm-number 643 --erir-contragent-tin-type LEGAL --erir-contragent-tin 1234567890 --dry-run direct --login CLIENT_LOGIN clients update --phone +70000000000 --notification-email [email protected] --dry-run direct agencyclients add-passport-organization --name "Org" --currency RUB --notification-email [email protected] --notification-lang EN --no-send-account-news --send-warnings --dry-run direct agencyclients add-passport-organization-member --passport-organization-login org-login --role CHIEF --invite-email [email protected] --dry-run direct agencyclients update --client-id 42 --phone +70000000000 --notification-email [email protected] --grant EDIT_CAMPAIGNS=YES --grant IMPORT_XLS=NO --dry-run
direct agencyclients add is runtime-deprecated by Yandex Direct and is blocked by the CLI. Use direct agencyclients add-passport-organization instead.
dynamicads update is unsupported by API. The Yandex Direct
dynamictextadtargets service exposes add, get, delete, suspend,
resume, and setBids, but no update operation. Do not add or rely on
direct dynamicads update unless Yandex exposes a real API method.
All get commands support --format:
| Format | Description |
|---|---|
json |
JSON (default) |
table |
Formatted table |
csv |
CSV |
tsv |
TSV |
direct campaigns get --format table direct campaigns get --format csv --output campaigns.csv
direct campaigns get --limit 10 # first 10 results direct campaigns get --fetch-all # all pages
The following commands make irreversible changes — use with caution:
| Command | Effect |
|---|---|
campaigns delete --id |
Permanently deletes a campaign and all its contents |
adgroups delete --id |
Permanently deletes an ad group |
ads delete --id |
Permanently deletes an ad |
keywords delete --id |
Permanently deletes a keyword |
audiencetargets delete --id |
Permanently deletes an audience target |
Commands that affect live ad delivery: suspend, resume, archive, unarchive (available on campaigns, ads), suspend, resume (also on keywords, and on adgroups as an emulation over the group's ads — see the Ad Groups section above for the resume caveat).
Commands that affect bids and spending: bids set, keywordbids set, bidmodifiers set.
Use --dry-run on add / update commands to preview the API request before sending:
direct campaigns add --name "Test" --start-date 2024-01-01 --dry-runYandex Direct can return a successful HTTP response that still contains
item-level Errors for one object. Direct CLI treats those responses as
failed operations: it exits non-zero and prints the error code, message, and
details.
Code 8800 with Object not found usually means the object is not available
under the current Client-Login or account. Check the selected --login,
YANDEX_DIRECT_LOGIN, or auth profile before retrying.
Four tiers of tests live under tests/:
| Tier | Marker | Network | Token required |
|---|---|---|---|
| Unit / CLI wiring / dry-run | (none) | No | No |
| Read-only integration | -m integration |
Yes (production API, read-only) | Yes |
| Write integration | -m integration_write |
No (replays VCR cassettes) | No |
| Live draft write integration (v5) | -m integration_live_write |
Yes when recording, otherwise VCR replay | Yes + YANDEX_DIRECT_LIVE_WRITE=1 |
| v4 live read | -m v4_live_read |
Yes (production v4 JSON API, read-only) | Yes |
| v4 live account-level report write (opt-in) | -k _opt_in_write in tests/test_v4_live_contracts.py |
Yes (production v4) | Yes + YANDEX_DIRECT_V4_LIVE_REPORT_WRITE=1 |
pip install -e ".[dev]" pytest # fast offline tier, parallel via xdist — no token pytest -n0 # same offline tier, sequential (for pdb / -s) pytest -m integration -v # read-only integration tests (needs token) pytest -m integration_write -v # write cassette replay (no token needed) YANDEX_DIRECT_LIVE_WRITE=1 pytest -m integration_live_write -v # live draft cassette replay (v5) YANDEX_DIRECT_LIVE_WRITE=1 pytest -m integration_live_write -v --record-mode=rewrite # re-record live draft cassette YANDEX_DIRECT_V4_LIVE_REPORT_WRITE=1 pytest tests/test_v4_live_contracts.py -k _opt_in_write -v # v4 wordstat/forecast account-level lifecycle
The v4 account-level write tier (YANDEX_DIRECT_V4_LIVE_REPORT_WRITE=1) creates real Wordstat and forecast reports in the production account and deletes them in the same run. There are no cassettes — these tests run against live API only. Created IDs are tracked in ~/.direct-cli/test-orphans.json so that if the run is interrupted between create and delete, the next invocation will retry the cleanup automatically (see tests/_orphan_store.py).
Every CLI subcommand is classified in direct_cli/smoke_matrix.py.
| Category | Script | When to run |
|---|---|---|
| SAFE | scripts/test_safe_commands.sh |
Production read-only smoke checks; requires YANDEX_DIRECT_TOKEN and YANDEX_DIRECT_LOGIN |
| WRITE_SANDBOX | scripts/test_sandbox_write.sh |
Live sandbox write smoke checks; requires YANDEX_DIRECT_TOKEN and YANDEX_DIRECT_LOGIN; reports PASS, FAIL, SANDBOX_LIMITATION, or NOT_COVERED for each command |
| DANGEROUS | scripts/test_dangerous_commands.sh |
Manual checklist only; exits with status 1 by design |
Current command surface:
| Metric | Count |
|---|---|
| WSDL-backed API services | 29 |
| Supported API services including Reports | 30 |
| WSDL operations | 112 |
CLI groups including auth |
40 |
CLI subcommands including auth |
144 |
API CLI subcommands excluding auth |
140 |
The project now distinguishes four surfaces:
| Surface | Coverage strategy |
|---|---|
| Canonical WSDL-backed SOAP services | tests/test_api_coverage.py verifies strict service/method parity and dry-run request-schema coverage or explicit exclusions |
| Live-discovered WSDL model gaps | scripts/build_api_coverage_report.py reports services seen in the audited live API surface but not yet declared in the CLI coverage model |
Non-WSDL services (reports) |
Explicit contract tests |
| Historical aliases retained by exception | None currently retained |
| Intentional CLI-only helpers | Explicitly allowlisted with reasons in direct_cli/wsdl_coverage.py |
100% coverage in this project means full coverage of the supported
declared canonical API surface. The API coverage report also includes a
model_gaps section for live-discovered Yandex Direct services that are not
yet part of that declared model. Alias groups and CLI-only helpers remain
supported, but they are tracked outside the strict parity metric.
Useful maintenance commands:
python scripts/build_api_coverage_report.py python scripts/refresh_wsdl_cache.py python scripts/check_wsdl_drift.py
CI runs a scheduled API coverage workflow that:
- runs the fast coverage suites;
- uploads a machine-readable API coverage report artifact, including declared parity and live-discovered model gap counts;
- checks the cached WSDL files against the live Yandex Direct API on schedule.
WRITE_SANDBOX smoke is a live check against the Yandex Direct sandbox.
It does not replay stored HTTP traffic and it does not create new recordings.
Run it only when you intentionally want to call api-sandbox.direct.yandex.ru:
set -a && source .env && set +a scripts/test_sandbox_write.sh
The runner executes matrix commands through direct --sandbox ..., creates
temporary sandbox prerequisites where possible, and cleans them up best-effort.
The report contains one row per WRITE_SANDBOX command:
PASSmeans the command completed against the live sandbox API.SANDBOX_LIMITATIONmeans the request reached the API and hit a known sandbox-only limitation such as codes8800,1000,3500, or5004.FAILmeans an unexpected CLI or API error.NOT_COVEREDmeans the runner does not yet know how to safely build the prerequisites for that command.
The same OAuth token works for both production and the sandbox; no separate sandbox token is needed.
For v4account sandbox smoke, enable-shared-account uses
YANDEX_DIRECT_V4ACCOUNT_CLIENT_LOGIN or falls back to YANDEX_DIRECT_LOGIN.
account-management requires YANDEX_DIRECT_V4ACCOUNT_ACCOUNT_ID; without it
the runner reports NOT_COVERED for that command.
clients.update is opt-in because it mutates client-level account metadata.
Set YANDEX_DIRECT_CLIENTS_UPDATE_LOGIN to an expendable sandbox
Client-Login; the runner passes it through --login and updates only
ClientInfo with a unique smoke marker. Without that variable, the runner
reports NOT_COVERED for clients.update.
The integration_write pytest tier still replays stored write-test traffic
for regression coverage. If you change those tests or their payloads and
intentionally need to refresh the fixtures, regenerate them separately:
set -a && source .env && set +a # load YANDEX_DIRECT_TOKEN / LOGIN pytest -m integration_write -v --record-mode=rewrite
After recording, always audit the generated YAMLs for leaked secrets:
grep -r "$YANDEX_DIRECT_TOKEN" tests/cassettes/ # must return nothing grep -r "$YANDEX_DIRECT_LOGIN" tests/cassettes/ # must return nothing
The VCR config in tests/conftest.py already strips Authorization,
Client-Login, cookies and any response header containing the substring
login, but manual verification is mandatory before committing.
The integration_live_write tier is manual-only and intentionally separate
from sandbox cassette tests. In rewrite mode it runs against the production
Yandex Direct API, but it may only create disposable draft resources and
delete the exact IDs it created in the same test run. Current coverage is
limited to a guarded campaign draft create -> get -> delete check.
Replay the checked-in cassette:
YANDEX_DIRECT_LIVE_WRITE=1 pytest -m integration_live_write -v
Re-record it only when you intentionally want to verify live draft behavior:
YANDEX_DIRECT_LIVE_WRITE=1 pytest -m integration_live_write -v --record-mode=rewrite
Do not add tests to this tier that accept external IDs, resume/suspend/archive existing resources, mutate bids, or touch serving campaigns.
Build, validate and upload to PyPI:
pip install -e ".[dev]" scripts/release_pypi.sh testpypi # upload to TestPyPI scripts/release_pypi.sh pypi # upload to PyPI scripts/release_pypi.sh all # both
The script reads credentials from .env:
TWINE_USERNAME=__token__ TEST_PYPI_TOKEN=pypi-... PYPI_TOKEN=pypi-...
PyPI API tokens can be account-wide or project-scoped:
- Project-scoped tokens only allow uploads to the specific project they were created for. A token scoped to
telethon-clicannot uploaddirect-cli— you will get 403 Forbidden. - Account-wide tokens allow uploads to any project under your account.
- For the first publication of a new project, you must use an account-wide token (project-scoped tokens cannot be created until the project exists on PyPI).
- After the first successful upload, create a project-scoped token at https://pypi.org/manage/account/token/ and replace the account-wide token in
.env.
Bump version in pyproject.toml before each release — PyPI rejects duplicate versions.
MIT