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.
- 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
- Log in to Speakness and open the Media Control Board for your outlet.
- Choose Settings in the menu, then the General tab.
- In the API Key card, click the eye icon to reveal the key and the copy button to copy it.
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"
}
| Field | Required | Description |
|---|---|---|
headline | Yes | Article title, up to 255 characters. |
articleUrl | Yes | The 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. |
externalId | Recommended | The article's ID in your CMS. Prevents duplicates and is the key for updates and existence checks. |
articleTypeName | No | "News Article" (default) or "Opinion Piece". See Article types. |
imageUrl | No | Lead image, shown where Speakness lists the article. |
categories | No | List of category names from your CMS. |
tags | No | List of tag names from your CMS. |
publishedAt | No | ISO 8601 timestamp. Defaults to the time of the request. |
journalistId | No | Speakness journalist ID, if you use journalist profiles. |
Response:
{ "articleId": 12345 }
| Status | Meaning |
|---|---|
200 | Article registered. |
400 | Validation failed (missing headline, invalid URL, …). |
401 | Missing, invalid or expired token. |
409 | An article with this externalId already exists for your media. Safe to ignore on re-publish. |
500 | The 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. |
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.
articleTypeName | Reader votes on the article | Use for |
|---|---|---|
News Article | Good Journalism and Good News | Reporting, news analysis. |
Opinion Piece | Civil, 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
externalIdalready exists are skipped, and so are articles whosearticleUrlis not under your registered site URL. - If any
articleTypeNamein 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
| Symptom | Likely cause |
|---|---|
Embed shows an error / 401 on the comment section | The site URL is not registered as a media yet, or this article has not been registered through POST /Article. |
409 on every publish | Expected when re-publishing; the article already exists. Use PUT /Article/{externalId} to change it. |
500 on POST /Article | articleUrl 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 comments | The embed's data-url differs from the registered articleUrl — query strings, trailing slashes and www all matter. |
401 on the API | Token expired (3 hours) or the API key was regenerated. Request a new token. |
Need help? Email support@speakness.com.