API Reference
Programmatic access to your leads, products, and scans. Available on Pro and Agency plans.
https://leadsrover.ioAuthentication
All requests require a Bearer token. Create API keys in Settings > API.
curl -H "Authorization: Bearer lr_sk_..." \ https://leadsrover.io/api/v1/leads
Keys have scopes: read (default) for GET requests, and write for mutations. Max 5 keys per account, revokable instantly.
Response format
All responses use a JSON envelope.
Success
{
"data": { ... }
}Error
{
"error": {
"code": "NOT_FOUND",
"message": "Lead not found",
"doc_url": "https://leadsrover.io/docs/api#errors"
}
}Paginated endpoints include a pagination object with total, limit, offset, and hasMore.
Rate limits
Exceeding the limit returns 429 with a RATE_LIMITED error code.
Errors
BAD_REQUESTInvalid parametersUNAUTHORIZEDMissing or invalid API keyFORBIDDENInsufficient scope or planLIMIT_REACHEDAI generation quota exhaustedNOT_FOUNDResource not found or not owned by youCONFLICTResource conflict (e.g. scan running, draft exists)RATE_LIMITEDToo many requestsINTERNAL_ERRORServer errorOpenAPI & agents
A machine-readable OpenAPI 3.1 spec covering every endpoint is available at /api/v1/openapi.json. Plug it into a ChatGPT Action, Claude, or any agent framework to give your AI assistant direct access to your leads.
Pair it with the agent guide — workflow instructions written for AI agents (triage loop, feedback semantics, the DM draft lifecycle). Paste its URL or contents into your agent's system instructions.
# Endpoint schemas curl https://leadsrover.io/api/v1/openapi.json # Workflow instructions for your agent curl https://leadsrover.io/api/v1/agent-guide
/api/v1/meReturns your account info and current plan.
{
"data": {
"user": {
"id": "abc123",
"name": "Jane Doe",
"email": "jane@example.com",
"timezone": "America/New_York",
"createdAt": "2026-01-15T10:30:00.000Z"
},
"plan": {
"type": "pro",
"status": "active",
"currentPeriodEnd": "2026-04-15T10:30:00.000Z",
"limits": {
"maxProducts": 5,
"scansPerMonth": -1,
"aiCommentsPerMonth": 1000
}
}
}
}/api/v1/productsLists all your products.
{
"data": [
{
"id": 42,
"name": "Acme CRM",
"url": "https://acme.com",
"pain": "Manual lead tracking is slow",
"solution": "AI-powered CRM for small teams",
"targetUsers": "Solo founders and small sales teams",
"isActive": true,
"createdAt": "2026-02-01T08:00:00.000Z",
"updatedAt": "2026-03-20T14:30:00.000Z"
}
]
}/api/v1/products/:idReturns a single product by ID. Same shape as the list endpoint.
/api/v1/products/:id/scanTrigger an on-demand scan. Requires write scope. Scans run asynchronously.
Response (202)
{
"data": {
"productId": 42,
"status": "scanning",
"message": "Scan enqueued successfully"
}
}/api/v1/leadsLists leads across all products with filtering and pagination.
productIdstatusminQualitysinceincludesortorderlimitoffsetExample
curl -H "Authorization: Bearer lr_sk_..." \ "https://leadsrover.io/api/v1/leads?minQuality=70&status=new&limit=10"
Response
{
"data": [
{
"id": 1234,
"productId": 42,
"subreddit": "smallbusiness",
"redditPostId": "t3_abc123",
"title": "Need a CRM that doesn't cost a fortune",
"authorUsername": "startup_jane",
"postUrl": "https://reddit.com/r/smallbusiness/...",
"qualityScore": 85,
"intent": "Looking for affordable CRM for 3-person team",
"upvotes": 12,
"commentCount": 8,
"postedAt": "2026-03-27T14:30:00.000Z",
"discoveredAt": "2026-03-27T15:00:00.000Z",
"status": "new",
"contactedAt": null,
"language": "en",
"matchedKeyword": "affordable crm",
"feedback": null,
"dislikeReason": null
}
],
"pagination": {
"total": 47,
"limit": 10,
"offset": 0,
"hasMore": true
}
}/api/v1/leads/:idReturns a single lead with its full post content. Same shape as the list endpoint, plus a content field with the full Reddit post body.
/api/v1/leads/:idUpdate a lead's status. Requires write scope. Marking a lead contacted records contact history and rejects any queued (unsent) drafts for it.
Request
{
"status": "contacted",
"contactMethod": "dm"
}Response
{
"data": {
"id": 1234,
"status": "contacted"
}
}Valid statuses: new contacted. Optional contactMethod: dm comment
If you queued a DM via POST /leads/:id/draft, do not mark the lead contacted yourself — that happens automatically when the draft is sent.
/api/v1/leads/:idDelete a lead (soft delete — it disappears from all listings). Requires write scope. Deleting is workflow cleanup, not quality feedback: to teach the scorer a lead was bad, submit a dislike first, then delete.
{
"data": {
"id": 1234,
"deleted": true
}
}Deleting an already-deleted lead returns 404.
/api/v1/leads/:id/feedbackLike or dislike a lead. Requires write scope. Dislikes with a concrete reason feed the learning loop and improve future scoring for the product. Submitting again replaces previous feedback.
feedbackreasonnoteRequest
{
"feedback": "dislike",
"reason": "seller_side",
"note": "Poster is an agency pitching services"
}Response
{
"data": {
"leadId": 1234,
"feedback": "dislike",
"dislikeReason": "seller_side",
"note": "Poster is an agency pitching services"
}
}/api/v1/leads/:id/feedbackClear your feedback on a lead. Idempotent — succeeds even when no feedback exists. Requires write scope.
{
"data": {
"leadId": 1234,
"feedback": null
}
}/api/v1/leads/:id/generate-dmGenerate a DM for a lead using your saved reply settings (including custom instructions). Returns text only — it does not create a draft. Review or rewrite the text, then queue it via POST /leads/:id/draft. Counts against your AI generation quota. Requires write scope.
{
"data": {
"leadId": 1234,
"dm": "Hey, saw your post about tracking leads manually...",
"quota": { "used": 14, "limit": 1000 }
}
}Returns 403 LIMIT_REACHED when the generation quota is exhausted.
/api/v1/products/:id/leadsLists leads for a specific product. Same filters as GET /leads except productId.
/api/v1/leads/:id/draftQueue a DM with your own text. Requires write scope. The draft is created approved and your browser extension sends it on its next poll — no in-app review step. Queuing is the send decision, so make sure the text is final before calling this.
contentdmSubjectRequest
{
"content": "Hey, saw your post about...",
"dmSubject": "About your CRM search"
}Response (201)
{
"data": {
"id": 567,
"leadId": 1234,
"status": "approved",
"draftType": "dm",
"dmRecipient": "startup_jane",
...
}
}One draft per lead — returns 409 CONFLICT if a draft already exists (any status), the lead was already contacted, or the author cannot receive DMs.
/api/v1/draftsLists your DM drafts. Use it to track drafts you created through the lifecycle: approved → posted or failed.
statusproductIdleadIdlimitoffset{
"data": [
{
"id": 567,
"productId": 42,
"leadId": 1234,
"status": "posted",
"draftType": "dm",
"generatedContent": "Hey, saw your post about...",
"editedContent": null,
"dmSubject": "About your CRM search",
"dmRecipient": "startup_jane",
"generatedAt": "2026-06-11T10:00:00.000Z",
"reviewedAt": "2026-06-11T12:00:00.000Z",
"postedAt": "2026-06-11T12:05:00.000Z",
"createdAt": "2026-06-11T10:00:00.000Z",
"errorMessage": null
}
],
"pagination": { "total": 3, "limit": 50, "offset": 0, "hasMore": false }
}Conversations
The inbox: once a lead replies to a DM, the thread becomes a conversation you can triage and answer. Designed so an agent can run the inbox: list what needs a reply, read the thread, send an answer.
/api/v1/conversationsLists conversations for your account. filter=needs_reply (the default) is the daily queue: threads where the lead is waiting on a human reply.
filterlimitoffset{
"data": [
{
"id": 88,
"recipientUsername": "startup_jane",
"recipientRedditId": "t2_abc123",
"leadId": 1234,
"productName": "Acme CRM",
"replyStateKey": "needs_you",
"replyStateLabel": "Needs you",
"replyStateExplanation": "They asked a question about pricing.",
"lastMessage": {
"body": "How much is the team plan?",
"isFromUs": false,
"sentAt": "2026-06-13T09:30:00.000Z"
},
"lastMessageAt": "2026-06-13T09:30:00.000Z",
"convertedAt": null,
"archivedAt": null,
"snoozedUntil": null,
"sendable": true,
"queuedReply": null
}
],
"pagination": { "total": 1, "limit": 50, "offset": 0, "hasMore": false }
}/api/v1/conversations/:idFull message history for one conversation, oldest first, plus the lead — the Reddit post that triggered it, including the full post body in content. That post is precious context for replying. Read it before answering. isFromUs tells you which side sent each message; lead is null if the conversation isn't tied to a lead.
{
"data": {
"conversation": {
"id": 88,
"recipientUsername": "startup_jane",
"leadId": 1234,
"sendable": true,
"queuedReply": {
"message": "The team plan is $49/mo for 5 seats.",
"status": "pending"
},
...
},
"lead": {
"id": 1234,
"subreddit": "startups",
"title": "How did you get your first 100 customers?",
"content": "I launched 3 months ago and I'm stuck at zero...",
"postUrl": "https://reddit.com/r/startups/comments/...",
"qualityScore": 72,
"intent": "...",
...
},
"messages": [
{
"id": 1,
"body": "Hey, saw your post about CRMs...",
"mediaUrl": null,
"isFromUs": true,
"sentAt": "2026-06-12T18:00:00.000Z",
"source": "manual"
},
{
"id": 2,
"body": "How much is the team plan?",
"mediaUrl": null,
"isFromUs": false,
"sentAt": "2026-06-13T09:30:00.000Z",
"source": null
}
]
}
}/api/v1/conversations/:idUpdate a conversation's state. Requires write scope. Archive a dead end (declined, not a fit, can't convert) to drop it out of the inbox — archived conversations no longer appear in the needs_reply queue.
archivedconvertedsnoozedUntilcancelQueuedReply{ "archived": true }