Content and opportunities
Read saved articles, export documents, update metadata, inspect scores, and list Reddit opportunities.
These endpoints use the product resolved from your API key or signed-in session. See Authentication for key setup, product selection, and shared authentication errors. All examples use Bash and placeholders, not live credentials.
Endpoint inventory
| Method and path | API key | Effect |
|---|---|---|
GET /api/content | Yes | List saved content or count pending exports |
GET /api/content/{id} | Yes | Read one saved article |
PATCH /api/content/{id} | Yes | Rename or change content metadata; paid-access gate |
GET /api/content/{id}/export | Yes | Download PDF or DOCX |
POST /api/content/{id}/unpublish | Yes | Unpublish on the remote provider; paid-access gate |
GET /api/content/search-stats | Yes | Read stored search statistics; may start a background sync |
GET /api/content/score | Yes | Read stored scores or bulk progress |
POST /api/content/score | No | Session-only paid AI scoring |
GET /api/opportunities | Yes | List Reddit opportunities; Pro/Max channel gate |
DELETE /api/opportunities/{id} | Yes | Soft-dismiss an opportunity; paid-access and channel gates |
There is no POST /api/content, DELETE /api/content/{id}, or GET /api/opportunities/{id} handler. The session-only routes POST /api/content/bulk-delete, POST /api/content/refresh-status, POST /api/content/editor/block-refine, PATCH /api/opportunities/{id}/status, and POST /api/opportunities/mark-commented are not API-key operations. Use the Loudmink interface for session-only actions.
List content
GET /api/content
| Query parameter | Default | Behavior |
|---|---|---|
count | Omitted | Exact true selects count-only mode and ignores the listing parameters |
page | 1 | Parsed integer, lower-clamped to 1 |
pageSize | 20 | Parsed integer, clamped to 1–100 |
search | Empty | Case-insensitive substring in title, summary, or filepath |
contentType | all | Otherwise an exact stored content-type filter |
filterStatus | all | Otherwise match computed published, modified, or unpublished |
sort | Empty | title, updatedAt, or aeoScore; other values use creation time descending |
sortDir | asc | Only exact desc selects descending for a recognized sort |
Send valid integers: malformed pagination values are not given a separate validation response. The default population excludes forum_post, but an explicit contentType replaces that exclusion. Count mode always excludes forum posts and articles with reviewStatus: "pending_review".
curl 'https://loudmink.ai/api/content?page=1&pageSize=20&sort=updatedAt&sortDir=desc' \
-H "Authorization: Bearer $LOUDMINK_API_KEY"The listing envelope is { items, total, page, pageSize, totalPages }. Each item has id, title, filepath, summary, parsed tags, primaryProvider, contentType, targetPlatform, hasRemoteMapping, remotePublishStatus, reviewStatus, lastExportedAt, updatedAt, aeoScore, parsed scoreBreakdown, scoredAt, and publishStatus. url, remoteUrl, and syncStatus can be omitted. Times are ISO strings; unset scores/times and unsupported review states are null.
Synthetic response subset:
{
"items": [{ "id": "content_example", "title": "Example article", "publishStatus": "unpublished", "remotePublishStatus": null }],
"total": 1,
"page": 1,
"pageSize": 20,
"totalPages": 1
}publishStatus is a sync/export bucket, not proof a remote article is public. A remote mapping with synced or remote_modified status belongs to the published bucket even when remotePublishStatus is draft. Use remotePublishStatus (draft, published, removed, or null) for the known remote state. The current target platform's non-local mapping is preferred. remoteUrl is exposed only from mappings explicitly marked remotely published.
Without a remote mapping, an export at or after the last update yields published; a later local update yields modified. Remote local_modified/conflict mappings yield modified; a mapping marked remotely removed yields unpublished.
A status-filtered list computes status over at most the first 5,000 matching rows, then paginates in memory. Its total is limited to that population. The unfiltered list uses database pagination without that 5,000-row cap. Unknown non-all status values match no derived status. Count mode returns only { count }, counting unpublished plus modified articles eligible for bulk export, without pagination.
Read an article
GET /api/content/{id}
The ID must belong to the resolved organization and product. No body or query parameters are used. This reads saved data without scoring, publishing, or a paid-access gate.
curl 'https://loudmink.ai/api/content/CONTENT_ID' \
-H "Authorization: Bearer $LOUDMINK_API_KEY"Success is { success: true, article } with Cache-Control: private, no-store. article contains id, title, summary, stored Markdown content, filepath, url, tags, contentType, targetPlatform, reviewStatus, aeoScore, scoredAt, factSources, includeSources, createdAt, and updatedAt. Tags are string arrays, with malformed legacy tags normalized to []. Fact sources are parsed entries with numeric n and string url; absent/malformed source data becomes []. The stored Markdown is not a rendered export or a live provider fetch.
Stored fact-source entries can additionally carry title, addedBy (model or user), claimText, quote, and status: "approved". The read parser only requires n and url, so legacy entries need not contain every modern field.
Errors: 404 with { success: false, error: "Article not found." }; 500 with { success: false, error: "Failed to read article." }.
Update article metadata
PATCH /api/content/{id}
Requires writable paid product access. This route is not a general Markdown editor.
| JSON field | Behavior |
|---|---|
title | If a string, selects the rename branch; trimmed, nonempty, maximum 200 characters |
targetPlatform | Updated when present only if the title branch is not selected |
contentType | Updated when present only if the title branch is not selected |
curl -X PATCH 'https://loudmink.ai/api/content/CONTENT_ID' \
-H "Authorization: Bearer $LOUDMINK_API_KEY" \
-H 'Content-Type: application/json' \
-d '{"title":"An updated article title"}'A rename updates the database title and stored Markdown frontmatter, keeps local mappings' native frontmatter in step, and marks all sync mappings local_modified. If there is no non-local mapping, it also changes the filepath to a title-derived .md slug unless that would collide. A non-local mapping prevents filepath renaming even if the remote article is a draft. It does not rename or update the live remote post.
Rename success: { success: true, updated: { id, title, filepath } }. Metadata success: { success: true, updated: { id, targetPlatform, contentType } }. A string title takes precedence, so send separate requests to rename and change metadata. The metadata branch has no route-level enum validation for platform/type; this is not a promise that arbitrary values are supported elsewhere.
Errors include 400 for empty/overlong string titles, 404 for a missing article in the rename branch, shared paid-access failures, and 500 { success: false, error: "Failed to update content" }. Invalid JSON, incompatible metadata types, or a missing article in the metadata branch reach the generic 500 catch rather than a dedicated validation/404 response.
Export an article
GET /api/content/{id}/export?format=pdf
format defaults to pdf, is lowercased, and accepts only pdf or docx. No paid-access gate is added here. The successful response is binary, not JSON, with Content-Disposition: attachment, a title-derived filename, Content-Length, and Cache-Control: no-store.
| Format | Content-Type |
|---|---|
pdf | application/pdf |
docx | application/vnd.openxmlformats-officedocument.wordprocessingml.document |
curl 'https://loudmink.ai/api/content/CONTENT_ID/export?format=docx' \
-H "Authorization: Bearer $LOUDMINK_API_KEY" \
--output article.docxThe export uses stored Markdown without frontmatter, with a legacy file-store fallback when empty. Source links/footer are rendered on the outbound document when includeSources is enabled; otherwise recognized markers are stripped. Stored content is not rewritten, and this download does not set lastExportedAt.
JSON errors use { success: false, error }: 400 unsupported format, 404 article not found, 422 no content to export, 500 export failure.
Unpublish an article
POST /api/content/{id}/unpublish
No request body is used. Requires active paid product access. This is a real remote write: the selected provider's unpublish operation takes the post out of public publication. It prefers the active non-local mapping matching targetPlatform, falling back to another active mapping, and never selects mappings already marked removed.
curl -X POST 'https://loudmink.ai/api/content/CONTENT_ID/unpublish' \
-H "Authorization: Bearer $LOUDMINK_API_KEY"Success: { success: true, providerType }. The mapping is preserved, with remotePublishStatus: "draft", syncStatus: "synced", and a refreshed remote-check time. The content row and target platform are not deleted. This differs from deleting a content record, and from the list's computed sync bucket.
Service failures return 400 { success: false, error }, including missing content, no eligible remote mapping, workspace restrictions, and provider errors. Unexpected route errors return 500 with "Failed to unpublish".
Search Console statistics
GET /api/content/search-stats
No parameters are used. Returns { connected: false } when there is no selected Search Console property, the connection does not belong to the product, or the workspace is a demonstration workspace. Otherwise returns { connected: true, stats, pending }.
stats maps content IDs to { clicks, impressions, position, clicksTrend?, impressionsTrend?, positionTrend? }. Metrics cover the current stored 28-day window ending three days before today. Clicks/impressions are sums; position is impression-weighted, with an unweighted fallback when impressions are zero. Trends are current minus prior 28-day totals/position and are omitted when there is no prior data. Articles without current-window rows are absent.
This GET can write: a never-seeded or more-than-24-hour-old integration starts background Search Console syncing, updating stored rows and its sync timestamp. It returns the currently stored statistics immediately. pending is true only when the first seed has not completed; it does not mean every stale refresh is still running. Background failures are logged and leave first-seed polling eligible to retry.
Stored score and scoring progress
GET /api/content/score
Choose one mode:
| Query | Response |
|---|---|
contentIndexId=CONTENT_ID | { aeoScore, breakdown, scoredAt, isOutdated } |
progress=true&since=ISO_TIMESTAMP | { scored, total, done } |
Progress mode takes precedence only when progress is exactly true and a nonempty since is supplied. scored counts product content with scoredAt at or after that timestamp; total counts all product content, and done means scored >= total. It is not a job ID/status endpoint and does not exclude forum posts. Supply a valid timestamp: there is no dedicated date-validation response.
Single-score retrieval parses the stored breakdown and does not rescore. breakdown and scoredAt can be null; isOutdated is null when no score exists, otherwise indicates whether content was updated after scoring. Missing ID/mode returns 400 { error: "contentIndexId or progress param is required" }; unknown content returns 404 { error: "Content not found" }.
An article breakdown has numeric queryFit, headingStructure, entityDensity, faqPresence, freshness, and internalLinks, plus string-array insights, optional type: "article", and optional eeat (experience, expertise, authoritativeness, trust). A forum breakdown uses type: "forum", numeric authenticity, credibility, specificity, formatCompliance, freshness, and string-array insights. Legacy stored breakdowns may lack newer optional fields.
curl 'https://loudmink.ai/api/content/score?contentIndexId=CONTENT_ID' \
-H "Authorization: Bearer $LOUDMINK_API_KEY"Session-only scoring write
POST /api/content/score is not supported by API keys. Score articles inside Loudmink; a key-only request is not a supported way to start scoring.
Scoring in Loudmink is subject to active access, available credits and cooldowns. Use the stored-score GET above when your integration only needs to read an existing result.
List Reddit opportunities
GET /api/opportunities
Requires the product's effective Pro or Max Reddit-channel entitlement. The API returns 403 { success: false, error: "Reddit channel requires a Pro or Max plan." } otherwise, including for residual rows after a downgrade.
| Query parameter | Default | Behavior |
|---|---|---|
page | 1 | Parsed integer, minimum 1 |
pageSize | 20 | Parsed integer, clamped to 1–100 |
range | month | Exact all disables the first-seen date filter; otherwise rolling last 30 days, not calendar month |
filter | all | Otherwise exact stored status, normally new, viewed, commented, ignored |
search | Empty | Case-insensitive title/subreddit substring |
sort | Empty | subreddit, lastSeen, mentioned, cited, status, engines |
sortDir | desc | Only exact asc selects ascending for a recognized sort |
Default/unknown sort uses lastSeenAt descending. cited sorts stored impactScore; mentioned sorts productMentioned; status and engines use stored order/count columns. Numeric pagination is not separately validated for malformed input.
curl 'https://loudmink.ai/api/opportunities?range=all&pageSize=20&filter=new' \
-H "Authorization: Bearer $LOUDMINK_API_KEY"Response: { opportunities, total, page, pageSize, totalPages, statusCounts, totalQueries, totalEnginesOnPlan, redditUsed, redditCap }.
Each opportunity contains id, threadUrl, draftedThisMonth, subreddit, threadTitle, productMentioned, timesSeen, status, citedByEngines, impactScore, citedByQueries, firstSeenAt, and lastSeenAt. citedByQueries is an array of { id, queryText }; engine keys are filtered to those allowed by the plan. draftedThisMonth uses canonical thread identity. redditUsed is the number of threads drafted in the usage period and redditCap is the workspace plan cap; neither is the number of visible rows.
statusCounts has all, new, viewed, commented, and ignored. Counts respect the date range but not search or the selected status filter. The default list and counts exclude dismissed opportunities. Use the documented status filters for customer reporting. Generic failures return 500 { success: false, error: "Failed to fetch opportunities" }.
Dismiss a Reddit opportunity
DELETE /api/opportunities/{id}
No body is used. Requires writable paid access and the Pro/Max Reddit channel. The ID must belong to the resolved product and organization.
curl -X DELETE 'https://loudmink.ai/api/opportunities/OPPORTUNITY_ID' \
-H "Authorization: Bearer $LOUDMINK_API_KEY"Success is { success: true }. This writes status: "dismissed" and statusOrder: 4, not a hard delete. The tombstone preserves history and keeps the thread out of the default list even if detection cites it again. It does not refund prior credit usage, post on Reddit, or complete a reply. Unknown/out-of-scope IDs return 404 { success: false, error: "Opportunity not found" }; unexpected failures return 500 "Failed to delete opportunity" in the same envelope.
Safe content workflow
Read the list, retrieve an article, and download its PDF/DOCX without triggering scoring. A metadata PATCH is a saved-content write, not publishing. Only call unpublish when intentionally taking the remote post offline. Use stored-score GETs for integration reads; scoring POST and Reddit status/comment completion remain application-session actions.