PromoPilot API

Unified REST API of the PromoPilot ecosystem: launch link building, track positions, conduct SEO audits and security scans from your code — CRM, SaaS, agency dashboards.

  • Base URL: https://promopilot.link/api/v1
  • Format — JSON over HTTPS; all responses contain an ok field.
  • Projects created via the API are shown in the dashboard under a separate section 'API Projects' and are marked with a badge — they do not mix with personal projects.
  • Machine-readable specification: openapi.json
curl https://promopilot.link/api/v1/ping
# {"ok":true,"pong":true,"time":"2026-07-20T12:00:00Z"}

Authorization and keys

API keys are issued in the client dashboard: Dashboard → Developer API. When creating a key, you choose which services it has access to (scopes): promotion, rank, audit, shield. The key is shown fully only once — we store only its SHA-256 fingerprint.

Authorization: Bearer ppk_ваш_ключ
# или
X-Api-Key: ppk_ваш_ключ
Security: store the key in environment variables or a secret store, do not publish it in client code and repositories. Compromised keys should be revoked immediately in the dashboard — revocation is instant. Use different keys with the minimally required scopes for different integrations.

Billing and balance

  • All paid operations are deducted from the personal account balance — the same as in the dashboard. There is no separate 'API balance'.
  • Promotion: price by tariff / flexible cascade / global price, minus your personal discount; Pro subscribers automatically receive an additional 10% discount.
  • Before launching, you can find out the exact price: POST /promotion/quote (without deduction).
  • Rank Tracker — deduction based on actual checks; SEO Audit full — prepayment based on size estimate; Shield deep/comprehensive — fixed scan price.
  • If funds are insufficient — HTTP 402 insufficient_funds with fields required, balance, shortfall and a link to top up topup_url. Balance top-up is done in the dashboard.

Error format

{
  "ok": false,
  "error": {
    "code": "insufficient_funds",
    "message": "Insufficient funds in the balance. Please top up your balance in the dashboard and retry the request.",
    "details": {"required": 44.1, "balance": 10.0, "shortfall": 34.1, "topup_url": "…"}
  }
}
CodeHTTPDescription
unauthorized401Key not provided, not found, or revoked
forbidden_scope403The key does not have access to this service — issue a key with the required scope
pro_required403Feature available on Pro plan (e.g., white-label branding)
forbidden403Action forbidden (e.g., deleting a personal project via API)
not_found404Object not found or belongs to another account
insufficient_funds402Insufficient funds: details will include required, balance, shortfall, and topup_url
validation_error422Invalid parameters; field specified in details.field
domain_mismatch422Link does not lead to the project domain
already_running409Process is already running (e.g., project audit)
needs_verify409Shield: domain not verified
need_estimate409Audit: first an express audit for size assessment
rate_limited429Request limit exceeded; please retry after Retry-After seconds
report_archived410Report archived due to retention period; Pro gets access automatically
level1_disabled503Promotion temporarily disabled by the platform
api_disabled503Public API temporarily disabled
server_error500Internal error — please try again later

Limits

  • 120 requests per minute per key (basic limit).
  • Launching promotions — up to 20 per minute; creating projects — up to 30 per hour; launching audits/scans — up to 10 per minute.
  • If exceeded — HTTP 429 with Retry-After header. Recommended polling frequency for statuses — once every 30–60 seconds.

Localization

Human-readable messages (message) are returned in the language from the ?lang= parameter (ru, en, uk, pl) or the Accept-Language header; by default — the language of your account. Machine error codes and statuses are not translated — bind your logic to them.

Endpoints Reference

Account

GET /me
Profile: balance, currency, personal discount, Pro status, key scopes.
Example Response
{"ok":true,"user":{"id":2,"balance":124.50,"currency":"USD","promotion_discount_percent":10,"is_pro":true,...},"key":{"label":"CRM","scopes":["promotion"]}}
GET /balance
Current balance and link to top-up.
Example Response
{"ok":true,"balance":124.50,"currency":"USD","topup_url":"…/client/balance.php"}
GET /key
Information about the presented key: scopes, usage statistics.
Example Response
{"ok":true,"key":{"label":"CRM","scopes":["promotion","rank"],"requests_total":1520,...}}

Projects and Links scope: promotion

GET /projects
List of projects. By default — only those created via API; ?all=1 — including personal ones.
ParameterWhereDescription
all query «1» — return both personal and API projects
Example Response
{"ok":true,"projects":[{"id":311,"name":"Client A","created_via":"api","links_count":3,"active_runs":1,...}]}
POST /projects
Create a project. It will be marked as created_via=api and will appear in the dashboard under 'API Projects'. You can also submit links right away — their domain must match the project's domain.
ParameterWhereDescription
name * body Project Name
url * body Website address (homepage)
language body Content language (ru/en/uk/pl/…), default is ru
region body Audience region
topic body Topic
wishes body Text wishes
links body Array of objects {url, anchor, language, wish}
Example Response
{"ok":true,"project":{"id":312,"created_via":"api",...},"links":{"added":2,"domain_errors":0}}
GET /projects/{id}
Project with links and recent launches.
Example Response
{"ok":true,"project":{"id":312,"links":[...],"runs":[...]}}
PATCH /projects/{id}
Update name / description / language / wishes / region / topic.
Example Response
{"ok":true,"project":{...}}
DELETE /projects/{id}
Delete project. Allowed only for projects created via API.
Example Response
{"ok":true,"deleted":true}
POST /projects/{id}/links
Add promoted link. The link's domain must match the project's domain (or be its subdomain) — otherwise, domain_mismatch error.
ParameterWhereDescription
url * body Full URL of the page
anchor body Anchor (leave empty — we will choose automatically)
language body Page language
wish body Text wish for this link
Example Response
{"ok":true,"link":{"id":915,"url":"https://site.com/page",...},"links_count":4}
DELETE /projects/{id}/links/{linkId}
Remove link from project.
Example Response
{"ok":true,"deleted":true}

Promotion (cascades) scope: promotion

GET /promotion/tariffs
Active rates (for tariff_id), unit prices of flexible cascade, global price, and your personal discount.
Example Response
{"ok":true,"tariffs":[{"id":3,"slug":"pro","name":"Pro","price_per_link":49.0,"cascade":{...}}],"your_discount_percent":10}
POST /promotion/quote
Pre-calculation of launch cost without deduction — the same parameters as for the launch.
ParameterWhereDescription
tariff_id body Tariff ID
level1_count…crowd_per_article body Flexible cascade parameters
Example Response
{"ok":true,"pricing_mode":"tariff","base_price":49.0,"discount_percent":10,"total":44.1,"balance":124.5,"sufficient_funds":true}
POST /promotion/runs
Start link promotion. Cost will be deducted from personal balance (discounts and Pro bonus are applied automatically). Restarting the same link with an active cascade will return already_active.
ParameterWhereDescription
project_id * body Project ID
url * body Promoted link (or pass link_id)
link_id body Project link ID — an alternative to url
tariff_id body Launch by tariff
level1_count body Flexible cascade: L1 publications
level2_per_level1 body L2 for each L1
level3_per_level2 body L3 for each L2
crowd_per_article body Crowd links to the article
schedule_start_at body Delayed start (ISO-datetime)
Example Response
{"ok":true,"run_id":501,"status":"queued","charged":44.1,"discount_percent":10,"balance_after":80.4,"currency":"USD"}
GET /promotion/runs
List of launches with pagination. Filters: project_id, status (active | queued | running | completed | failed | …), initiated_via (api | web).
ParameterWhereDescription
page, per_page query Pagination (up to 100 per page)
Example Response
{"ok":true,"runs":[{"id":501,"status":"level1_active","progress":{"done":3,"total":12,"pct":25},...}],"pagination":{...}}
GET /promotion/runs/{id}
Launch status: stage, progress, breakdown by levels (total / success / verified).
Example Response
{"ok":true,"run":{"id":501,"status":"level2_active","levels":[{"level":1,"total":5,"verified":5},...]}}
GET /promotion/runs/{id}/nodes
All cascade placements: network, publication URL, verification status. ?verified=1 — only confirmed links.
ParameterWhereDescription
verified query «1» — only verified placements
Example Response
{"ok":true,"run_id":501,"nodes":[{"level":1,"network":"telegraph","url":"https://telegra.ph/…","verified":true,...}]}

Reports and white-label (Pro) scope: promotion

GET /promotion/runs/{id}/report
Final report (only verified placements, as in the dashboard). ?format=pdf will return a link to the PDF. On the Pro plan, the report is branded with your white-label (see /branding); fields can be overridden with brand_* parameters.
ParameterWhereDescription
format query json (default) or pdf
brand_company query Pro: company name in header
brand_logo_url query Pro: logo URL
brand_accent_color query Pro: HEX accent color
brand_footer_text query Pro: footer text
Example Response
{"ok":true,"run":{...},"summary":{"verified":12,...},"report":{"level1":[...],"level2":[...],"crowd":[...]},"whitelabel":{...}} — или {"ok":true,"pdf":{"url":"…/uploads/reports/api-cascade-501-….pdf"},"branded":true}
GET /branding
Current white-label brand and Pro status.
Example Response
{"ok":true,"is_pro":true,"branding_active":true,"branding":{"company_name":"Acme","logo_url":"…","accent_color":"#6366f1",...}}
PUT /branding
Save white-label brand (Pro only). The same brand is used in the dashboard (Settings → White-label).
ParameterWhereDescription
company_name * body Company name (required)
logo_url, website, email, phone, address, footer_text, accent_color body Other brand fields
Example Response
{"ok":true,"branding":{...}}

Rank Tracker scope: rank

GET /rank/projects
Position monitoring projects.
Example Response
{"ok":true,"projects":[{"id":7,"domain":"site.com","region":"UA","keywords_count":50,"top10":12,...}]}
POST /rank/projects
Create project: domain, region (UA/US/PL/…), device (desktop/mobile).
ParameterWhereDescription
domain * body Domain or website URL
name body Name (default — domain)
region body Search region, default UA
device body desktop | mobile
Example Response
{"ok":true,"project":{"id":8,...}}
GET /rank/projects/{id}
Project + all keywords with latest positions.
Example Response
{"ok":true,"project":{...},"keywords":[{"id":91,"keyword":"купить окна","last_position":4,"last_checked_at":"…"}]}
POST /rank/projects/{id}/keywords
Add keywords: newline-separated string or array.
ParameterWhereDescription
keywords * body String "kw1\nkw2" or array of strings
tag body Group label
target_url body Target page
Example Response
{"ok":true,"added":25,"skipped":0}
POST /rank/projects/{id}/check
Queue position check (all words or keyword_ids[]). Billing is based on actual checks at internal service rates. Retrieve results from GET /rank/projects/{id}.
ParameterWhereDescription
keyword_ids body Array of word IDs (default — all)
Example Response
{"ok":true,"queued":50,"note":"…"}

SEO Audit scope: audit

GET /audit/projects
Audit projects.
Example Response
{"ok":true,"projects":[{"id":4,"domain":"site.com","start_url":"https://site.com/",...}]}
POST /audit/projects
Create audit project.
ParameterWhereDescription
url * body Website address
name body Name
include_subdomains body true — scan subdomains
Example Response
{"ok":true,"project":{"id":5,...}}
GET /audit/projects/{id}/estimate
Website size assessment and full audit price (at least one express audit is needed for assessment).
Example Response
{"ok":true,"has_estimate":true,"estimated_pages":430,"full_audit_price":5.0,"currency":"USD"}
POST /audit/projects/{id}/crawls
Start audit. express — free (daily limit), full — complete, prepayment from balance based on size assessment.
ParameterWhereDescription
mode body express (default) | full
Example Response
{"ok":true,"crawl_id":88,"mode":"full","max_pages":500,"charged":5.0}
GET /audit/crawls/{id}
Status and summary: Health Score, errors/warnings, issue summary by category (when completed).
Example Response
{"ok":true,"crawl":{"id":88,"status":"done","health_score":79,"pages_crawled":430,"issues":{...}}}
GET /audit/projects/{id}/crawls
Project audit history.
Example Response
{"ok":true,"crawls":[...]}

Security Shield scope: shield

GET /shield/projects
Protected sites.
Example Response
{"ok":true,"projects":[{"id":3,"domain":"site.com","verified":true,...}]}
POST /shield/projects
Create/find a project by URL. The response will include verify_token for domain verification.
ParameterWhereDescription
url * body Website address
name body Name
Example Response
{"ok":true,"project":{"id":3,"verified":false,"verify_token":"pp-verify-…"},"verification_hint":"…"}
POST /shield/projects/{id}/verify
Check domain verification (DNS TXT record, .well-known file, or meta tag with verify_token). Required for paid scans.
Example Response
{"ok":true,"verified":true,"method":"dns"}
POST /shield/projects/{id}/scans
Start a scan: free (basic, daily limit), deep, or comprehensive — deducted from balance.
ParameterWhereDescription
mode body free | deep | comprehensive
lang body AI report language (ru/uk/en/pl)
Example Response
{"ok":true,"scan_id":41,"mode":"deep","charged":19.0}
GET /shield/scans/{id}
Status, score/grade, traffic light verdict, summary of findings by severity, and public link to the report.
Example Response
{"ok":true,"scan":{"id":41,"status":"done","score":86,"grade":"B","verdict":{...},"findings_summary":{"critical":0,"high":1,...},"share_report_url":"…"}}

Code Examples

cURL — create a project and start promotion

curl -X POST https://promopilot.link/api/v1/projects \
  -H "Authorization: Bearer ppk_ВАШ_КЛЮЧ" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Client Site",
    "url": "https://client-site.com",
    "language": "ru",
    "links": [{"url": "https://client-site.com/services", "anchor": "услуги компании"}]
  }'
curl -X POST https://promopilot.link/api/v1/promotion/runs \
  -H "Authorization: Bearer ppk_ВАШ_КЛЮЧ" \
  -H "Content-Type: application/json" \
  -d '{"project_id": 312, "url": "https://client-site.com/services"}'

PHP

<?php
$key = getenv('PROMOPILOT_API_KEY'); // никогда не храните ключ в коде
function pp_api(string $method, string $path, array $data = null) {
    global $key;
    $ch = curl_init('https://promopilot.link/api/v1' . $path);
    curl_setopt_array($ch, [
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_CUSTOMREQUEST => $method,
        CURLOPT_HTTPHEADER => [
            'Authorization: Bearer ' . $key,
            'Content-Type: application/json',
            'Accept-Language: ru',
        ],
        CURLOPT_POSTFIELDS => $data !== null ? json_encode($data) : null,
    ]);
    $res = json_decode(curl_exec($ch), true);
    $code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    curl_close($ch);
    if (($res['ok'] ?? false) !== true) {
        throw new RuntimeException($res['error']['code'] ?? ('http_' . $code));
    }
    return $res;
}

$project = pp_api('POST', '/projects', ['name' => 'Client', 'url' => 'https://client-site.com']);
try {
    $run = pp_api('POST', '/promotion/runs', [
        'project_id' => $project['project']['id'],
        'url' => 'https://client-site.com/services',
    ]);
    echo "Запущено, run_id={$run['run_id']}, списано {$run['charged']}";
} catch (RuntimeException $e) {
    if ($e->getMessage() === 'insufficient_funds') {
        // пополните баланс и повторите
    }
}

Python

import os, requests

API = "https://promopilot.link/api/v1"
HEADERS = {"Authorization": f"Bearer {os.environ['PROMOPILOT_API_KEY']}"}

def api(method, path, **json_body):
    r = requests.request(method, API + path, headers=HEADERS, json=json_body or None, timeout=30)
    data = r.json()
    if not data.get("ok"):
        code = data.get("error", {}).get("code", f"http_{r.status_code}")
        if code == "insufficient_funds":
            details = data["error"]["details"]
            raise RuntimeError(f"Пополните баланс: не хватает {details['shortfall']}")
        raise RuntimeError(code)
    return data

project = api("POST", "/projects", name="Client", url="https://client-site.com")
run = api("POST", "/promotion/runs", project_id=project["project"]["id"],
          url="https://client-site.com/services")
print("run_id:", run["run_id"], "charged:", run["charged"])

# Поллинг статуса
import time
while True:
    st = api("GET", f"/promotion/runs/{run['run_id']}")["run"]
    print(st["status"], st["progress"]["pct"], "%")
    if st["status"] in ("completed", "failed", "cancelled"):
        break
    time.sleep(60)

report = api("GET", f"/promotion/runs/{run['run_id']}/report")

Node.js

const API = "https://promopilot.link/api/v1";
const KEY = process.env.PROMOPILOT_API_KEY;

async function api(method, path, body) {
  const res = await fetch(API + path, {
    method,
    headers: {
      "Authorization": `Bearer ${KEY}`,
      "Content-Type": "application/json",
      "Accept-Language": "ru",
    },
    body: body ? JSON.stringify(body) : undefined,
  });
  const data = await res.json();
  if (!data.ok) {
    const code = data.error?.code || `http_${res.status}`;
    if (code === "insufficient_funds") {
      console.error("Пополните баланс:", data.error.details.topup_url);
    }
    throw new Error(code);
  }
  return data;
}

const { project } = await api("POST", "/projects", { name: "Client", url: "https://client-site.com" });
const run = await api("POST", "/promotion/runs", { project_id: project.id, url: "https://client-site.com/services" });
console.log("run:", run.run_id, "charged:", run.charged);

OpenAPI

Full machine-readable specification for client generation and import into Postman/Insomnia: https://promopilot.link/api/v1/openapi.json

Questions and suggestions about the API — through support in the dashboard or Telegram. Backward compatibility: breaking changes are only released in a new version (/api/v2), current fields are not removed without an announcement in the changelog.

Ready to get started?
Create an API key in the dashboard — it will take a minute.
Get API key