PromoPilot API
A single REST API for the whole PromoPilot ecosystem: launch link promotion, track rankings, run SEO audits and security scans from your own code — CRM, SaaS or an agency panel.
- Base URL:
https://promopilot.link/api/v1
- The format is JSON over HTTPS; every response contains an ok field.
- Projects created via the API appear in the dashboard in a separate “API projects” section and carry a badge — they are not mixed with personal ones.
- Machine-readable specification: openapi.json
curl https://promopilot.link/api/v1/ping
# {"ok":true,"pong":true,"time":"2026-07-20T12:00:00Z"}
Authorisation and keys
API keys are issued in the client dashboard: Dashboard → API for developers. When creating a key you choose which services it may access (scopes): promotion, rank, audit, shield. The key is shown in full only once — we store nothing but its SHA-256 fingerprint.
Authorization: Bearer ppk_your_key# или
X-Api-Key: ppk_your_key
Security: keep the key in environment variables or a secret store, and never publish it in client-side code or repositories. Revoke a compromised key immediately in the dashboard — revocation takes effect at once. Use separate keys with the minimum necessary scopes for different integrations.
Billing and balance
- All paid operations are charged to the account’s personal balance — the same one as in the dashboard. There is no separate “API balance”.
- Promotion: the price comes from the plan, the flexible cascade or the global price, minus your personal discount; Pro subscribers automatically get an extra 10% off.
- You can get the exact price before launching: POST /promotion/quote (no charge).
- Rank Tracker is charged for the checks actually performed; SEO Audit full is prepaid based on the size estimate; Shield deep/comprehensive has a fixed scan price.
- If there are not enough funds you get HTTP 402 insufficient_funds with the fields required, balance, shortfall and a topup_url link. Topping up is done in the dashboard.
Error format
{
"ok": false,
"error": {
"code": "insufficient_funds",
"message": "Not enough funds on your balance. Top it up in the dashboard and repeat the request.",
"details": {"required": 44.1, "balance": 10.0, "shortfall": 34.1, "topup_url": "…"}
}
}
| Code | HTTP | Description |
unauthorized | 401 | The key was not supplied, was not found, or has been revoked |
forbidden_scope | 403 | The key has no access to this service — issue a key with the required scope |
pro_required | 403 | The feature requires the Pro plan (for example, white-label branding) |
forbidden | 403 | The action is forbidden (for example, deleting a personal project via the API) |
not_found | 404 | The object was not found or belongs to another account |
insufficient_funds | 402 | Insufficient funds: details will contain required, balance, shortfall and topup_url |
validation_error | 422 | Invalid parameters; the field is named in details.field |
domain_mismatch | 422 | The link does not point to the project domain |
already_running | 409 | The process is already running (an audit of the project, for example) |
needs_verify | 409 | Shield: the domain is not verified |
need_estimate | 409 | Audit: run an express audit first to estimate the size |
rate_limited | 429 | Rate limit exceeded; retry after Retry-After seconds |
report_archived | 410 | The report has been archived by the retention policy; Pro accounts get access automatically |
level1_disabled | 503 | Promotion is temporarily disabled by the platform |
api_disabled | 503 | The public API is temporarily disabled |
server_error | 500 | Internal error — please retry later |
Limits
- 120 requests per minute per key (the base limit).
- Promotion launches — up to 20 a minute; project creation — up to 30 an hour; audit and scan launches — up to 10 a minute.
- On exceeding the limit you get HTTP 429 with a Retry-After header. The recommended status polling interval is once every 30–60 seconds.
Localization
Human-readable messages (message) come back in the language from the ?lang= parameter (ru, en, uk, pl) or the Accept-Language header; by default, your account language. Machine error codes and statuses are never translated — bind your logic to those.
Endpoint 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
The current balance and a top-up link.
Example response
{"ok":true,"balance":124.50,"currency":"USD","topup_url":"…/client/balance.php"}
GET
/key
Information about the presented key: scopes and usage statistics.
Example response
{"ok":true,"key":{"label":"CRM","scopes":["promotion","rank"],"requests_total":1520,...}}
Projects and links scope: promotion
GET
/projects
A list of projects. By default only those created via the API; ?all=1 includes personal ones.
| Parameter | In | Description |
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 is marked created_via=api and appears in the dashboard under “API projects”. You may pass links right away — their domain must match the project domain.
| Parameter | In | Description |
name * |
body |
Project Name |
url * |
body |
Site address (home page) |
language |
body |
Content language (ru/en/uk/pl/…), ru by default |
region |
body |
Audience region |
topic |
body |
Topic |
wishes |
body |
Notes for the copy |
links |
body |
An array of {url, anchor, language, wish} objects |
Example response
{"ok":true,"project":{"id":312,"created_via":"api",...},"links":{"added":2,"domain_errors":0}}
GET
/projects/{id}
The project with its links and latest runs.
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 a project. Allowed only for projects created via the API.
Example response
{"ok":true,"deleted":true}
POST
/projects/{id}/links
Add a link to promote. The link domain must match the project domain (or be a subdomain of it) — otherwise you get a domain_mismatch error.
| Parameter | In | Description |
url * |
body |
The full page URL |
anchor |
body |
Anchor (leave empty and we pick one automatically) |
language |
body |
Page language |
wish |
body |
Copy notes for this particular link |
Example response
{"ok":true,"link":{"id":915,"url":"https://site.com/page",...},"links_count":4}
DELETE
/projects/{id}/links/{linkId}
Remove a link from the project.
Example response
{"ok":true,"deleted":true}
GET
/promotion/tariffs
Active plans (for tariff_id), flexible-cascade unit prices, the 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
A price estimate for a launch without charging — the same parameters as the launch itself.
| Parameter | In | Description |
tariff_id |
body |
Plan 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
Launch promotion for a link. The cost is charged to your personal balance (discounts and the Pro bonus apply automatically). Launching the same link again while a cascade is active returns already_active.
| Parameter | In | Description |
project_id * |
body |
Project ID |
url * |
body |
The link to promote (or pass link_id) |
link_id |
body |
The project link ID — an alternative to url |
tariff_id |
body |
Launch on a plan |
level1_count |
body |
Flexible cascade: L1 publications |
level2_per_level1 |
body |
L2 per each L1 |
level3_per_level2 |
body |
L3 per each L2 |
crowd_per_article |
body |
Crowd links per article |
schedule_start_at |
body |
Deferred start (ISO date-time) |
Example response
{"ok":true,"run_id":501,"status":"queued","charged":44.1,"discount_percent":10,"balance_after":80.4,"currency":"USD"}
GET
/promotion/runs
A paginated list of runs. Filters: project_id, status (active | queued | running | completed | failed | …), initiated_via (api | web).
| Parameter | In | Description |
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}
Run status: stage, progress and a per-level breakdown (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 placements in the cascade: network, publication URL, verification status. ?verified=1 returns confirmed links only.
| Parameter | In | Description |
verified |
query |
“1” — verified placements only |
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
The final report (verified placements only, as in the dashboard). ?format=pdf returns a link to a PDF. On the Pro plan the report carries your white-label branding (see /branding); the fields can be overridden with brand_* parameters.
| Parameter | In | Description |
format |
query |
json (default) or pdf |
brand_company |
query |
Pro: company name in the header |
brand_logo_url |
query |
Pro: logo URL |
brand_accent_color |
query |
Pro: accent colour in HEX |
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
The 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 the white-label brand (Pro only). The same brand applies in the dashboard (Settings → White-label).
| Parameter | In | Description |
company_name * |
body |
Company name (required) |
logo_url, website, email, phone, address, footer_text, accent_color |
body |
The remaining brand fields |
Example response
{"ok":true,"branding":{...}}
Rank Tracker scope: rank
GET
/rank/projects
Rank monitoring projects.
Example response
{"ok":true,"projects":[{"id":7,"domain":"site.com","region":"UA","keywords_count":50,"top10":12,...}]}
POST
/rank/projects
Create a project: domain, region (UA/US/PL/…), device (desktop/mobile).
| Parameter | In | Description |
domain * |
body |
Site domain or URL |
name |
body |
Name (the domain by default) |
region |
body |
Search region, UA by default |
device |
body |
desktop | mobile |
Example response
{"ok":true,"project":{"id":8,...}}
GET
/rank/projects/{id}
The project plus all keywords with their latest positions.
Example response
{"ok":true,"project":{...},"keywords":[{"id":91,"keyword":"buy windows","last_position":4,"last_checked_at":"…"}]}
POST
/rank/projects/{id}/keywords
Add keywords: a newline-separated string or an array.
| Parameter | In | Description |
keywords * |
body |
A “kw1\nkw2” string or an 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 a rank check (all keywords or keyword_ids[]). You are charged for the checks actually performed at the service’s internal rates. Collect the results from GET /rank/projects/{id}.
| Parameter | In | Description |
keyword_ids |
body |
An array of keyword IDs (all by default) |
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 an audit project.
| Parameter | In | Description |
url * |
body |
Site address |
name |
body |
Name |
include_subdomains |
body |
true — crawl subdomains |
Example response
{"ok":true,"project":{"id":5,...}}
GET
/audit/projects/{id}/estimate
The site size estimate and the price of a full audit (at least one express audit is needed for the estimate).
Example response
{"ok":true,"has_estimate":true,"estimated_pages":430,"full_audit_price":5.0,"currency":"USD"}
POST
/audit/projects/{id}/crawls
Start an audit. express is free (daily limit); full is the complete audit, prepaid from your balance based on the size estimate.
| Parameter | In | Description |
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 results: Health Score, errors/warnings and a summary of issues by category (once complete).
Example response
{"ok":true,"crawl":{"id":88,"status":"done","health_score":79,"pages_crawled":430,"issues":{...}}}
GET
/audit/projects/{id}/crawls
The project’s 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 or find a project by URL. The response contains a verify_token for domain verification.
| Parameter | In | Description |
url * |
body |
Site 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 (a DNS TXT record, a .well-known file or a 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 — the latter two are charged to your balance.
| Parameter | In | Description |
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, rating (score/grade), the traffic-light verdict, a summary of findings by severity, and a public report link.
Example response
{"ok":true,"scan":{"id":41,"status":"done","score":86,"grade":"B","verdict":{...},"findings_summary":{"critical":0,"high":1,...},"share_report_url":"…"}}
Code samples
cURL — create a project and launch promotion
curl -X POST https://promopilot.link/api/v1/projects \
-H "Authorization: Bearer ppk_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Client Site",
"url": "https://client-site.com",
"language": "ru",
"links": [{"url": "https://client-site.com/services", "anchor": "company services"}]
}'
curl -X POST https://promopilot.link/api/v1/promotion/runs \
-H "Authorization: Bearer ppk_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"project_id": 312, "url": "https://client-site.com/services"}'
PHP
<?php
$key = getenv('PROMOPILOT_API_KEY'); // never keep the key in your code
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 "Started, run_id={$run['run_id']}, charged {$run['charged']}";
} catch (RuntimeException $e) {
if ($e->getMessage() === 'insufficient_funds') {
// top up the balance and retry
}
}
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"Top up your balance: short by {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"])
# Status polling
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("Top up your balance:", 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
The full machine-readable specification for generating clients and importing into Postman/Insomnia: https://promopilot.link/api/v1/openapi.json
Questions and suggestions about the API go through support in the dashboard or Telegram. Backward compatibility: breaking changes ship only as a new version (/api/v2), and current fields are never removed without an announcement in the changelog.
Ready to start?
Create an API key in the dashboard — it takes a minute.
Get an API key