# PiarFlow API

> PiarFlow monetizes Telegram bot traffic: a bot shows users sponsor subscription tasks from PiarFlow and the bot owner earns for every counted subscription. This file summarizes the public HTTP API for developers and AI coding assistants. Human-readable docs: https://piarflow.com/api-docs

Base URL: `https://piarflow.com/v1`. All requests and responses are JSON. Errors return an HTTP status and `{"status": "error", "message": "..."}`.

## Keys

- **API token** (user level). Issued in the Telegram bot @PiarFlowBot: Личный кабинет → Интеграция API. Can be regenerated there; the old one stops working at once. Used for `/traffic_bot*`, `/orders*` and `/balance`.
- **Bot API key** (per traffic bot). Returned by `POST /traffic_bot/add`. Used for `/sponsors` and `/sponsors/check`.

Both are sent as `Authorization: Bearer <key>`. Keep them server-side (environment variables), never in frontend code.

## Main flow

1. User opens the bot (e.g. /start).
2. Bot server calls `POST /sponsors` with the user's Telegram ID.
3. Bot shows each returned `link` as a URL button plus a "Check" button.
4. On "Check", bot server calls `POST /sponsors/check` with the same links.
5. If every item has `status: "subscribed"`, grant access; otherwise show the unfinished links again.

## Methods

### POST /sponsors — get tasks (bot API key)

Body: `user_id` (int, required), `chat_id` (int, required), `max_sponsors` (int, optional). If the bot was connected without a Telegram bot token, also send `first_name`, `username` (without @), `language_code`, `bio` — use `null` when unknown.

Response: `{"status": "ok", "sponsors": [{"link": "https://t.me/...", "status": "unsubscribed", "price": 5}]}`

### POST /sponsors/check — check tasks (bot API key)

Body: `user_id` (int), `links` (string[] from /sponsors).

Response: `{"status": "ok", "sponsors": [{"link": "...", "status": "subscribed"}]}`

Statuses: `subscribed` — done; `unsubscribed` — not done; `not_counted` — done but not paid.

### POST /traffic_bot/add — register a traffic bot (API token)

Header `Authorization: Bearer <API token>`. Body: `bot_token` (Telegram bot token from BotFather). The bot is owned by the API token's account. Response contains `api_key` (bot API key) and `bot` object (`bot_id`, `chat_id`, `username`, `title`, `topic`, `is_active`, `max_sponsors`, `reset_time`, `sold_subs`, `not_counted`, `earned`). 409 if the bot belongs to another user.

### POST /traffic_bot/api_key — get bot API key (API token)

Body: `bot_token`. Registers the bot if needed. Response: `api_key`, `bot`.

### GET /traffic_bot — bot info

Either bot API key alone, or API token plus `bot_id` query parameter (bot must belong to the token owner).

### GET|POST /traffic_bot/stats — bot stats for a day

Parameters: `date` (YYYY-MM-DD, Moscow time) for one day, or `date_from` + `date_to` (inclusive, max 30 days) for a period; the response carries `date` or `date_from`/`date_to` accordingly. Same auth as the `/traffic_bot`. Response `stats`: `sold_subs`, `earned`, `sponsor_tasks_requested`, `sponsor_tasks_checked`.

## Bot factories (API token)

For services that run many bots. All methods use `Authorization: Bearer <API token>` and only see the token owner's bots (others → 404). Base `https://piarflow.com/v1/factory`. A bot object has the fields of `/traffic_bot` plus `api_key`.

- `POST /factory/bots` — body `bot_token`, optional `max_sponsors` (1–10), `reset_time` (minutes, 5–5760). Registers or refreshes the bot, returns `{"status", "message", "bot"}`. 409 if the bot belongs to another user.
- `GET /factory/bots?offset=0&limit=20` — `limit` 1–100. Returns `total`, `offset`, `limit`, `bots`.
- `GET /factory/bots/{bot_id}` — one bot.
- `PATCH /factory/bots/{bot_id}` — body with any of `is_active`, `max_sponsors`, `reset_time`. 403 when enabling a banned, moderated or rejected bot.
- `POST /factory/bots/{bot_id}/api_key` — issues a new bot API key, the old one stops at once. Returns `api_key`.

## Advertisers: orders (API token)

An order buys subscribers for a Telegram channel/chat, a Telegram bot or an external site. Nothing is charged on creation; each counted subscriber is paid from the advertiser balance. Only the token owner's orders are visible (others → 404).

- `GET /orders/meta` — allowed values: `types`, `resource_check_modes`, `topics`, `excludable_topics`, `places`, `countries` (`code`/`name`), `genders`, `age_categories`, `tg_premium_modes`, `min_price`, `min_price_tg_premium_only`, `total_limit_min` (50), `limit_max`, `rate_interval_max_minutes` (1440), `creation_cooldown_seconds` (60), `checker_bot`.
- `GET /orders` — `{"orders": [{"id", "type", "title", "link", "is_active"}]}`.
- `POST /orders` — body `type` (`channel`|`resource`|`bot`), `link` (channel `@name`/`https://t.me/...`; bot ref link `https://t.me/<bot>?start=...`; resource `https://...`), `bot_token` (bot orders), `resource_check_mode` (resource orders: `assumption` — default, shown paired behind a checked sponsor and counted when that sponsor is done; `track` — issued alone through a track link and counted 3 s after the user passes it), `topic` (from `meta.topics`, optional), `total_limit` (0 = unlimited, else ≥ 50). Returns 201 with the full order and `created_for_moderation`. For channels, first make `meta.checker_bot` an admin with the invite-users right, else 409 with `checker_bot`. One new order per 60 s (429 with `retry_after`). An existing order with the same link is restarted instead of duplicated. Price starts at the market average.
- `GET /orders/{id}` — `order`: `id`, `type`, `title`, `link`, `topic`, `is_active`, `is_awaiting_moderation`, `can_run`, `price`, `effective_price` (price plus fixed surcharges: age +0.20 RUB, 18+ +0.80 RUB, and subscriber-profile requirements: +0.20 RUB each, a counter adds +0.10 RUB for every level after the first), `payment_source`, `stats` (`subscribers`, `subscribers_today`, `paid_subscribers`, `spent`, `avg_subscriber_price`), `limits` (`total_limit`, `hourly_limit`, `rate` {`min`, `max`, `interval_minutes`}, `distribute_by_days`), `targeting` (`places`, `gender`, `age_category`, `tg_premium`, `countries`, `disabled_topics`). Money values are strings.
- `PATCH /orders/{id}` — any of: `price` (raised to the floor), `link`, `total_limit`, `hourly_limit`, `rate` {`min`, `max`, `interval_minutes`}, `distribute_by_days`, `places` (`chats`/`bots`/`contests`), `gender` (`any`/`male`/`female`), `age_category` (0 all, 1 under 13, 2 14-17, 3 18+), `tg_premium` (`any`/`only`), `countries` (codes, [] = all), `disabled_topics`. Returns the updated order.
- `POST /orders/{id}/start`, `POST /orders/{id}/stop` — start re-checks the checker bot / bot token, the total limit and the balance; failure → 409 with the reason.
- `DELETE /orders/{id}` — stops and hides the order.

Order errors: 400 invalid field, 401 token, 403 blocked account or disabled order type, 404 order, 409 conflict (checker bot, other channel link, cannot start), 422 Telegram rejected the channel or bot token, 429 creation cooldown.

## General (API token)

- `GET /balance` — `balance` (`deposited`, `earned`, `available_to_withdraw`, `max_order_charge` — the largest per-subscriber charge payable, taken whole from one balance), `totals` (`earned_all_time`, `referral_earned`, `withdrawn`, `spent_on_traffic`), `bots[]` (`bot_id`, `username`, `title`, `is_active`, `subscribers`, `earned`), `chats[]` (same with `chat_id`). Money values are strings.

## Unsubscribe webhook

Configured in @PiarFlowBot (cabinet → Webhook / API). PiarFlow POSTs `{"tg_user_id", "offer_link", "status": "unsubscribed", "chat_id", "bot_id"}` when a user unsubscribes; a test call includes `"test": true`. Reply with any 2xx within 10 seconds.

## Errors

400 bad request · 401 key/token missing or invalid · 403 user cannot do the task (blocked the bot, never started it) · 404 tasks or bot not found · 409 bot owned by another user · 429 too many requests · 500 server error.
