# Bazzly REST API

Bazzly finds Reddit posts where a product fits, drafts replies and DMs, posts comments through warmed-up accounts, and tracks everything in queues. This API exposes the same actions as the dashboard. It is designed to be driven by AI agents: every list returns plain JSON, every credit-spending action previews its cost before it charges, and the same key also works with the Bazzly MCP server at `https://api.bazzly.ai/mcp`.

- OpenAPI 3.1 specification: https://bazzly.ai/openapi.json
- Base URL: `https://api.bazzly.ai/public/v1`
- Auth: `Authorization: Bearer <api key>` on every request (`X-Bazzly-Api-Key: <api key>` also works)
- Format: JSON in, JSON out. Send `Content-Type: application/json` on POST, PATCH and PUT.
- Keys: created under Account Settings → API & MCP, available during the free trial. Each key has scopes: `read`, `write`, `top-ups`.
- Errors: standard HTTP codes with a JSON body `{ "statusCode", "message" }`. `401` missing or invalid key, `403` missing scope, `404` not found, `400` validation error (the message names the field), `429` a limit was hit: 120 requests per minute per key, or an order cap.
- Access: keys work during the free trial. A cancelled subscription loses API and MCP access when its paid period ends (`401`).

## Quick start

1. `GET /me` to confirm the key and see its scopes.
2. `GET /projects` to find the project id.
3. `GET /opportunities?filter=top-ranked&sort=score&limit=10` to see the best Reddit posts to reply to.
4. `POST /opportunities/{id}/generate-reply` with `{ "confirmCreditSpend": true }` to draft a comment (0.1 credits).
5. `POST /queue/reply` with the opportunity id, `deliveryMethod` and the text to post it (10 credits via a warmed-up account, free from the user's own account).
6. `GET /queue/reply` to watch it go live.

## Credits and confirmation

Actions that spend credits or money return a preview instead of acting until you confirm:

- Credit-spending calls (`generate-reply`, `generate-dm`, `POST /queue/reply` with `deliveryMethod: "high_karma"`, `POST /custom-tasks`, `POST /custom-tasks/upvotes`) return `{ "requiresConfirmation": true, "costs": { ... }, "message": "..." }` when called without `"confirmCreditSpend": true`. Repeat the call with the flag to perform it.
- Money-spending calls (`POST /projects/{id}/activate`, `POST /projects/{id}/credits/manual`) use `"confirmPaymentSpend": true` the same way.

Costs: a warmed-account comment is 10 credits, an upvote 0.2 credits, an AI draft (reply or DM) 0.1 credits. Comments posted from the user's own account through the browser extension are free.

## Account

### GET /me
Scope: `read`. Returns the account and the key in use: `{ id, email, name, apiKey: { id, name, scopes } }`.

## Projects

A project is one product: its profile, watched subreddits, Google searches, filters, DM template and credits. Most accounts have one.

### GET /projects
Scope: `read`. Returns `{ data: Project[] }`. Project fields include `id`, `product_name`, `product_url`, `product_description`, `status` (`active`, `draft`, `inactive`, `locked`), `icp`, `keywords`, `competitors`, `pain_points`, `key_benefits`, `key_features`, `watched_subreddits`, `dm_template`, `discover_score_threshold`, `discover_classifications`, `scoring_custom_instructions`, `reply_custom_instructions`.

### GET /projects/{projectId}
Scope: `read`. One project with the fields above.

### POST /projects
Scope: `write`. Body `{ "url": "https://yourproduct.com" }`. Bazzly analyzes the page and creates the project with a generated profile. The first project on an account is created active; later ones are created as `draft` and need `activate`. At most two drafts can exist at once.

### PATCH /projects/{projectId}
Scope: `write`. Partial update of the profile. Accepted fields and limits:

| Field | Type | Limits |
| --- | --- | --- |
| product_name | string | required non-empty if sent |
| product_url | string | valid URL |
| product_description | string | 70 to 500 characters |
| icp | string | 20 to 400 characters |
| keywords | string[] | up to 10, 50 chars each |
| competitors | string[] | up to 6, 50 chars each |
| pain_points | string[] | up to 5, 120 chars each |
| key_benefits | string[] | up to 5, 120 chars each |
| key_features | string[] | up to 6, 120 chars each |
| watched_subreddits | string[] | up to 10, with or without the r/ prefix |
| dm_template | string | 3 to 500 chars; variables allowed: {post_problem_summary}, {product_benefit} |
| discover_score_threshold | integer | 75 to 100, minimum fit score for opportunities |
| discover_classifications | string[] or null | which post classifications to surface |
| scoring_custom_instructions | string or null | up to 1500 chars |
| reply_custom_instructions | string or null | up to 1500 chars, guides AI reply drafts |

Unknown fields are rejected. Autopilot settings are not exposed here.

### POST /projects/{projectId}/activate
Scope: `top-ups`. Activates a draft or inactive project. Body `{ "confirmPaymentSpend": true, "profile": { ...same fields as PATCH... } }`. Without the flag it returns `{ requiresConfirmation: true, ... }` describing the prorated seat charge, if any. Additional active projects are billed as extra seats at the plan price.

### GET /projects/{projectId}/credits
Scope: `read`. Returns `{ balance: { monthly, topped_up, total } }`. Monthly credits reset each billing period; topped-up credits never expire.

### POST /projects/{projectId}/credits/manual
Scope: `top-ups`. Body `{ "amount": 50, "confirmPaymentSpend": true }`. Charges the card on file at 1 credit = 1 USD; `amount` is 5 to 1000. Without the flag it returns a preview.

### GET /projects/{projectId}/credits/auto
Scope: `top-ups`. Returns `{ enabled, threshold, amount, monthly_cap, spent_this_month, topup_email_enabled }`. `monthly_cap` is USD per UTC calendar month across auto top-ups (null = no cap); `spent_this_month` is what auto top-ups have charged since the 1st.

### PUT /projects/{projectId}/credits/auto
Scope: `top-ups`. Body `{ "auto_topup_enabled": true, "auto_topup_threshold": 20, "auto_topup_amount": 50 }`. Threshold 1 to 100, amount 25 to 250. Both are required when enabling. Auto top-up only fires while Autopilot is running. Optional `auto_topup_monthly_cap` (25 to 10000, or null to remove; none by default) caps auto top-ups per UTC calendar month; when the next top-up would exceed it, Autopilot pauses with reason `budget_cap_reached` and resumes on the 1st, when the cap is raised or removed, or after a manual top-up. Optional `auto_topup_email_enabled` (off by default) sends a receipt email for every auto top-up; the limit-reached email is always sent. If a new `auto_topup_amount` is larger than the cap, the cap is raised to four times the new amount; a cap below the amount is raised to the amount. Either is returned as `monthly_cap_raised_to`.

## Opportunities

An opportunity is a Reddit post scored against the project. Ids are prefixed: `ro-{uuid}` for a post found by subreddit monitoring ("recent"), `sp-{uuid}` for a post that ranks on Google for one of the project searches ("top-ranked"). Both kinds share the endpoints below.

### GET /opportunities
Scope: `read`. Query: `projectId` (uuid, optional), `filter` (`all`, `recent`, `top-ranked`; default `all`), `sort` (`score`, `time`; default `score`), `limit` (1 to 100, default 50), `offset`. Returns `{ data: Opportunity[], total, nextOffset }`. Each item includes the prefixed `id`, `source`, `projectId`, the post (`title`, `body`, `subreddit`, `author`, `url`, `score`, `numComments`, `createdUtc`), the match (`fitScore` 0 to 1, `fitReason`, `classification`), any `proposedReply`, and for top-ranked posts `serpQuery`, `serpPosition` and `estimatedTraffic`.

### GET /opportunities/{id}
Scope: `read`. One opportunity.

### PATCH /opportunities/{id}
Scope: `write`. Body `{ "proposed_reply": "..." }` (3 to 4000 chars, or an empty string to clear) and/or `{ "skip": true }` to dismiss it.

### POST /opportunities/{id}/generate-reply
Scope: `write`. Body `{ "confirmCreditSpend": true }`. Drafts a comment that answers the post and mentions the product where it fits, using the project's reply instructions. Returns `{ success, text, creditsSpent: 0.1 }`. Without the flag: a cost preview.

## Leads

A lead is a Reddit user who wrote a post that matched the project. DMs are sent from the user's own Reddit account by the Bazzly browser extension; the API queues them, it does not send them.

### GET /leads
Scope: `read`. Query: `projectId`, `sort` (`score`, `time`), `limit` (1 to 100, default 50), `offset`. Returns `{ data: Lead[], total, nextOffset }`. Each lead has `id`, `projectId`, `redditUsername`, `bestFitScore`, `proposedDm`, and `reasons[]` with the matching posts and fit reasons.

### GET /leads/{id}
Scope: `read`. One lead.

### PATCH /leads/{id}
Scope: `write`. Body `{ "proposed_dm": "..." }` (3 to 2000 chars) and/or `{ "skip": true }`.

### POST /leads/{id}/generate-dm
Scope: `write`. Body `{ "confirmCreditSpend": true }`. Fills the project DM template from the lead's posts. Returns `{ success, text, creditsSpent: 0.1 }`.

## Reply queue

Every requested comment. Own-account comments post when the user runs the browser extension; warmed-account comments post automatically, usually within a few hours. A comment on a search-ranking (SERP) thread, or a custom task on a post at least a week old, in a subreddit that is new to the posting account usually takes three to five days, because the account takes part in that subreddit first so the comment is more likely to stay up. Until a slot is booked the comment stays `queued` with no scheduled time.

### GET /queue/reply
Scope: `read`. Query: `projectId`, `status` (`queued` by default, `posted`, `removed`, `cancelled`, or `all`), `limit` (1 to 200, default 100), `offset`. Returns `{ data: ReplyQueueItem[], total, nextOffset }`. Rows include `id`, `source` (`own` or `external`), stored `status` (`pending`, `queued`, `sent`, `failed`, or `dismissed`), `content`, `reddit_url`, `sent_at`, `error_message`, `comment_credits_refunded`, `canCancel`, and `upvoteOrders`. `upvoteOrders` lists each upvote order on the comment as `{ id, quantity, status, creditsCost, creditsRefunded, upvotesDelivered, createdAt, completedAt }`: `upvotesDelivered` counts votes that registered on the public score, and undelivered votes show up in `creditsRefunded`. The cancellation flag reflects current eligibility; the cancel endpoint rechecks it atomically. It does not reserve a cancellation window.

### POST /queue/reply
Scope: `write`. Body:

```json
{
  "opportunityId": "ro-...",
  "deliveryMethod": "high_karma",
  "content": "The comment text (3 to 4000 chars, English, no links, bare domains, subreddit links (r/name) or disguised links (like name dot com) for high_karma). Optional if a proposed_reply is already saved.",
  "upvotes": { "quantity": 5, "mode": "rank_first" },
  "confirmCreditSpend": true
}
```

`deliveryMethod` is `high_karma` (posted through a warmed-up account, 10 credits) or `extension` (queued for the user's own account, free). `upvotes` is optional: `quantity` 1 to 10, `mode` `custom` or `rank_first` (add enough to beat the current top comment). Without `confirmCreditSpend` the call returns `{ requiresConfirmation: true, costs: { highKarmaComment, upvotes, total } }`. On success: `{ success: true, externalTaskId | outreachQueueId, creditsSpent }`.

Rules the server enforces: warmed-account comments cannot contain links; strict-on-promotion subreddits may reject warmed-account posting unless the post is asking for tools; at most 10 warmed-account orders per project per day.

### POST /queue/reply/{id}/cancel
Scope: `write`. Cancels a queued own-account comment or an eligible warmed-account comment before posting preparation starts, including while waiting for a slot. Not every warmed-account comment supports cancellation: check `canCancel` in the queue response. Paid cancellation atomically refunds the comment and its pending upvotes and returns `{ success: true, refunded: true }`. Repeating a successful paid cancellation is safe. If preparation has started or cancellation is unavailable, returns `400` without cancelling or refunding. This does not delete a published Reddit comment.

### POST /queue/reply/{id}/retry
Scope: `write`. Requeues a removed or cancelled comment. Requeuing a warmed-account comment costs 10 credits again and may require edited text.

## DM queue

### GET /queue/dm
Scope: `read`. Query: `projectId`, `status` (`queued`, `sent`, `failed`), `limit`, `offset`. Returns `{ data: DmQueueItem[], total, nextOffset }`.

### POST /queue/dm
Scope: `write`. Body `{ "source": "lead", "sourceId": "<lead id>" }` or `{ "source": "opportunity", "sourceId": "ro-..." }` (top-ranked posts cannot be DMed). Uses the saved `proposed_dm`; generate or PATCH it first. Queueing is free; the DM is sent when the user runs the extension.

### POST /queue/dm/{id}/cancel
Scope: `write`.

### POST /queue/dm/{id}/retry
Scope: `write`. Requeues a failed DM.

## Custom tasks

### GET /custom-tasks/thread
Scope: `read`. Query: `redditUrl` (post, comment or `/s/` share link). Reads a thread before you write `content` for `POST /custom-tasks`. Returns `{ redditUrl, targetType, subreddit, post: { title, body, author, createdUtc }, parentComment, comments: [{ author, body, score, createdUtc }], postable, unpostable, subredditRules }`. `redditUrl` is the canonical form to submit. `parentComment` is set when the URL points at a comment. `comments` holds up to 10 existing comments the reply would sit beside (top-level for a post, direct replies for a comment), each cut at 1500 chars. `postable: false` means `POST /custom-tasks` would reject the URL, and `unpostable` says why. `subredditRules` is `{ promoStrictness, removalRefund, note, warmedAccountReplies: { available, reason, message } }`: `promoStrictness` is `strict`, `moderate`, `lenient` or `unknown`; `removalRefund: false` (strict subreddits) means a comment removed there is not refunded; `warmedAccountReplies.available: false` means an order there would be declined and refunded, with `reason` one of `approved_members_only`, `removes_new_comments` or `account_requirement`. A subreddit Bazzly has not seen before is read and classified during this call (the first lookup for it takes a few seconds longer), so `unknown` means it could not be read. Spends no credits. Limited to 30 lookups per 10 minutes per workspace (429 beyond that).

### POST /custom-tasks
Scope: `write`. Posts a comment on any Reddit post or comment by URL through a warmed-up account. This call always posts a comment. For upvotes without a comment use `POST /custom-tasks/upvotes`.

The comment text is either supplied or drafted:

- **Supplied**: send `content` and it is posted as written. Read the thread first with `GET /custom-tasks/thread`.
- **Drafted**: omit `content`. Bazzly writes the comment from the project profile and queues it in the same call, so the text is not reviewed before it is committed. This adds 0.1 credit, and the text comes back as `generatedReply`.

```json
{
  "projectId": "<uuid>",
  "redditUrl": "https://www.reddit.com/r/SaaS/comments/.../",
  "content": "Optional comment text (3 to 4000 chars, English). Omit to have Bazzly draft it.",
  "upvotes": { "quantity": 3, "mode": "custom" },
  "confirmCreditSpend": true
}
```

Same confirmation and cost rules as `POST /queue/reply`. Every call costs the warmed-account comment fee. Optional `upvotes` are delivered to the new comment once it is published. Links, bare domains, subreddit links (r/name), disguised links (like name dot com), non-English text and near-duplicates of a warmed-account comment from the last 30 days in `content` are rejected (fleet-wide for 8+ words, same subreddit and last 7 days only for shorter text); an order that was declined or cancelled before posting does not count as a duplicate. A comment removed within 48 hours of posting is refunded, except in a subreddit that is strict on promotion: the cost preview and the result both include `subredditRules` (same shape as `GET /custom-tasks/thread`), and `removalRefund: false` means a removal there is not refunded. The preview only reports what Bazzly already knows about the subreddit, so for one it has not seen call `GET /custom-tasks/thread` first. Returns `{ success, customTaskId, externalTaskId, generatedReply, creditsSpent, subredditRules }`.

### POST /custom-tasks/upvotes
Scope: `write`. Orders upvotes on an existing Reddit post or comment by URL. No comment is written or posted.

```json
{
  "projectId": "<uuid>",
  "redditUrl": "https://www.reddit.com/r/SaaS/comments/.../",
  "quantity": 3,
  "confirmCreditSpend": true
}
```

`quantity` is 1 to 10, at 0.2 credits per upvote, and delivery starts right away. Only one active upvote order per URL per workspace. Without `confirmCreditSpend: true` returns a cost preview. Requires an active paid subscription. Returns `{ success, customTaskId, externalTaskId, upvoteOrderId, creditsSpent }`. The order appears in `GET /queue/reply?status=posted` under `externalTaskId`.

## Help center

Answer product questions from the same articles the help center renders, instead of guessing or sending the user to support.

### GET /help/search
Scope: `read`. Query: `q` (2 to 200 chars), `limit` (1 to 10, default 5). Returns `{ query, results: [{ slug, title, excerpt, category, url, score, snippet }], helpCenterUrl }`. The snippet is the most relevant paragraph, often enough to answer from.

### GET /help/articles
Scope: `read`. Lists every article (`slug`, `title`, `excerpt`, `category`, `url`, `updatedAt`).

### GET /help/articles/{slug}
Scope: `read`. One article with its full markdown `body`.

## Feedback

### POST /feedback
Scope: `read` (any key). Report a bug, a missing capability, a docs gap or an unanswered question on the user's behalf. Do this yourself when something blocks you; the team reads every submission and no human needs to write to support.

```json
{
  "category": "bug | missing_feature | docs | question | other",
  "message": "What happened, what you expected (3 to 4000 chars)",
  "projectId": "<uuid, optional>",
  "context": { "endpoint": "POST /queue/reply", "status": 400, "note": "optional free-form details" }
}
```

Returns `{ id, receivedAt, message }`.

## MCP

The same capabilities are available as MCP tools at `https://api.bazzly.ai/mcp` (Streamable HTTP, bearer auth with the same key). Setup for Claude Code, Codex, Cursor and other clients: https://bazzly.ai/mcp

## Support

support@bazzly.ai
