Machine-readable version: https://bazzly.ai/docs/api.md

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:

FieldTypeLimits
product_namestringrequired non-empty if sent
product_urlstringvalid URL
product_descriptionstring70 to 500 characters
icpstring20 to 400 characters
keywordsstring[]up to 10, 50 chars each
competitorsstring[]up to 6, 50 chars each
pain_pointsstring[]up to 5, 120 chars each
key_benefitsstring[]up to 5, 120 chars each
key_featuresstring[]up to 6, 120 chars each
watched_subredditsstring[]up to 10, with or without the r/ prefix
dm_templatestring3 to 500 chars; variables allowed: {post_problem_summary}, {product_benefit}
discover_score_thresholdinteger75 to 100, minimum fit score for opportunities
discover_classificationsstring[] or nullwhich post classifications to surface
scoring_custom_instructionsstring or nullup to 1500 chars
reply_custom_instructionsstring or nullup 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:

{
  "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.
{
  "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.

{
  "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.

{
  "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