Skip to main content

Article API

Speakness identifies every comment thread by the article's URL, and it only loads a comment section for articles your media organization has registered. The WordPress plugin registers articles automatically. If you use the JavaScript embed from a custom CMS, your system registers them through the Article API described here.

At a glance
  • Audience: Developers integrating a custom CMS with Speakness.
  • When it runs: Once per article, when it is published (plus optional updates).
  • What you need: Your media's API key from the Speakness dashboard.

Base URL and authentication​

All requests go to:

https://api.speakness.com

Step 1: Find your API key​

  1. Log in to Speakness and open the Media Control Board for your outlet.
  2. Choose Settings in the menu, then the General tab.
  3. In the API Key card, click the eye icon to reveal the key and the copy button to copy it.
Keep the key secret

The API key authenticates your whole media organization. Store it server-side in your CMS only — never in page HTML or client-side JavaScript. It is not the same thing as the embed script.

Step 2: Exchange the key for a token​

Article endpoints accept a short-lived bearer token, not the API key itself.

POST /Auth/media-api-key-token
Content-Type: application/json

{ "apiKey": "<your API key>" }

Response:

{ "token": "eyJhbGciOi..." }

The token is valid for 3 hours. Cache it and request a new one when it expires (a 401 on any article call means the token is missing, invalid or expired).

Step 3: Call the Article API​

Send the token on every request:

Authorization: Bearer <token>

Your media organization is identified from the token, so it is never passed in the request body.


Register an article​

POST /Article
Authorization: Bearer <token>
Content-Type: application/json
{
"headline": "Council approves new harbour plan",
"articleUrl": "https://www.yoursite.com/news/harbour-plan-approved",
"imageUrl": "https://www.yoursite.com/images/harbour.jpg",
"externalId": "cms-article-48213",
"articleTypeName": "News Article",
"categories": ["News"],
"tags": ["harbour", "council"],
"publishedAt": "2026-09-04T10:00:00Z"
}
FieldRequiredDescription
headlineYesArticle title, up to 255 characters.
articleUrlYesThe article's canonical URL, up to 255 characters. Must start with the site URL you registered for your media (including www if your site uses it) and must not contain query strings. This exact URL goes into the embed's data-url.
externalIdRecommendedThe article's ID in your CMS. Prevents duplicates and is the key for updates and existence checks.
articleTypeNameNo"News Article" (default) or "Opinion Piece". See Article types.
imageUrlNoLead image, shown where Speakness lists the article.
categoriesNoList of category names from your CMS.
tagsNoList of tag names from your CMS.
publishedAtNoISO 8601 timestamp. Defaults to the time of the request.
journalistIdNoSpeakness journalist ID, if you use journalist profiles.

Response:

{ "articleId": 12345 }
StatusMeaning
200Article registered.
400Validation failed (missing headline, invalid URL, …).
401Missing, invalid or expired token.
409An article with this externalId already exists for your media. Safe to ignore on re-publish.
500The request could not be processed — most often because articleUrl is not under your registered site URL, or articleTypeName is not one of the two supported values.
Hook it into publishing

Call this endpoint from the same place your CMS decides to show the comment section (a "show comments" checkbox, a category rule, or every publish). Re-publishing the same article is harmless: you get a 409 and nothing changes.

Article types​

The type decides how readers vote on the article itself. It can only be set when the article is created — the update endpoint does not change it.

articleTypeNameReader votes on the articleUse for
News ArticleGood Journalism and Good NewsReporting, news analysis.
Opinion PieceCivil, Useful and Agree — three separate dimensions, so a reader can mark a piece as useful and well argued while disagreeing with it.Columns, editorials, op-eds.

Comment voting works the same way for both types.

Register many articles at once​

Useful for importing your archive.

POST /Article/batchArticles
Authorization: Bearer <token>
Content-Type: application/json

[
{ "headline": "...", "articleUrl": "...", "externalId": "...", "articleTypeName": "News Article" },
{ "headline": "...", "articleUrl": "...", "externalId": "...", "articleTypeName": "Opinion Piece" }
]
  • Up to 10,000 articles per request; the same fields as a single article.
  • Articles whose externalId already exists are skipped, and so are articles whose articleUrl is not under your registered site URL.
  • If any articleTypeName in the batch is invalid, the whole batch is rejected.

Response:

{ "response": { "insertedCount": 940, "existingCount": 60 } }

existingCount includes both already-registered articles and articles skipped for a mismatched URL.

Update an article​

PUT /Article/{externalId}
Authorization: Bearer <token>
Content-Type: application/json

{
"headline": "Council approves revised harbour plan",
"articleUrl": "https://www.yoursite.com/news/harbour-plan-approved",
"imageUrl": "https://www.yoursite.com/images/harbour-v2.jpg",
"categories": ["News"],
"tags": ["harbour", "council"],
"journalistId": null
}

Returns 200 when updated and 404 when no article with that externalId exists for your media. Send the full set of fields — this is a replacement, not a patch. The article type cannot be changed here.

Check whether articles are registered​

Single article:

GET /Article/check-article-exists/{externalId}
Authorization: Bearer <token>
{ "exists": true }

Several at once:

POST /Article/check-articles-exist
Authorization: Bearer <token>
Content-Type: application/json

["cms-article-48213", "cms-article-48214", "cms-article-48215"]
{ "results": [true, false, true] }

Results are in the same order as the IDs you sent.

Putting it together​

A minimal publish hook, as shell commands:

# 1. Token (cache this for up to 3 hours)
TOKEN=$(curl -s -X POST https://api.speakness.com/Auth/media-api-key-token \
-H "Content-Type: application/json" \
-d '{"apiKey":"YOUR_API_KEY"}' | jq -r .token)

# 2. Register the article
curl -s -X POST https://api.speakness.com/Article \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"headline": "Council approves new harbour plan",
"articleUrl": "https://www.yoursite.com/news/harbour-plan-approved",
"externalId": "cms-article-48213",
"articleTypeName": "News Article"
}'

Then render the embed on the page with the same URL:

<script src="https://www.speakness.com/embed-comment-section-prod.js"></script>
<div id="speakness-commentsection-embed" data-url="https://www.yoursite.com/news/harbour-plan-approved"></div>

Troubleshooting​

SymptomLikely cause
Embed shows an error / 401 on the comment sectionThe site URL is not registered as a media yet, or this article has not been registered through POST /Article.
409 on every publishExpected when re-publishing; the article already exists. Use PUT /Article/{externalId} to change it.
500 on POST /ArticlearticleUrl does not start with your registered site URL (check www vs no www, http vs https), or articleTypeName is misspelled.
Comment section is empty on a page that should have commentsThe embed's data-url differs from the registered articleUrl — query strings, trailing slashes and www all matter.
401 on the APIToken expired (3 hours) or the API key was regenerated. Request a new token.

Need help? Email support@speakness.com.