Article API
Endpoints for managing the "10-second race result summary" articles (article) tied to races.
Articles and races
Each article is tied through race_id to a race (_id) in one of the auto_racing /
bicycle_racing / horse_racing tables. Articles have no category (only blog / old_blog do).
Article status
| Status | Description |
|---|---|
| generating | Generating; AI article generation / build is in progress |
| draft | Draft (default) |
| published | Published |
| failed | Generation failed; the reason is in generation_error. Starting point for regeneration |
generating and failed are set by the generation pipeline. They cannot be supplied as input to
POST /v1/articles or PUT /v1/articles/{article_id} (doing so returns 400).
Article schema
| Field | Type | Notes |
|---|---|---|
| id | string | _id |
| race_id | string | _id of one of the racing tables |
| title | string | |
| body_html | string | HTML. For AI-generated articles, produced by the article_builder Lambda rendering the RaceSummary React component with renderToStaticMarkup |
| status | enum | generating / draft / published / failed. Default: draft |
| opening_at | number | Scheduled publish time. UNIX timestamp (ms) |
| closing_at | number | null | Scheduled unpublish time. UNIX timestamp (ms) |
| accuracy_test_data | object | null | Accuracy-verification data, read by scripts/article_accuracy_test/audit.py |
| generation_error | string | null | Failure reason when status=failed |
| custom_prompt | string | null | Extra instructions used by the last regeneration; prefilled in the input field |
| created_at | number | UNIX timestamp (ms) |
| updated_at | number | UNIX timestamp (ms) |
POST /v1/articles/generation
Overview
Generates an article with generative AI.
The article is created in generating state (with an empty body_html) and a generation job is
dispatched asynchronously to the article_generation Lambda through SQS (the article-generation
queue). The request returns 202 immediately and generation runs in the background. The result is
handed to the article_builder Lambda over another SQS queue (article-builder), which builds the
article HTML (body_html), stores it in DocumentDB and moves status to draft.
To create an article without AI, use POST /v1/articles.
Void / cancelled races cannot be generated
Races with no finishing order (void or cancelled) are rejected. With nothing to base the finishing order on, the AI invents a winner. Such requests return 422.
Request and Response
Headers
Request
Content-Type: application/json
Response
Content-Type: application/json
URI
Request body
{
"race_id": "string",
"title": "string",
"opening_at": 1234567890000,
"closing_at": 1234567890000
}
| Field | Type | Required | Notes |
|---|---|---|---|
| race_id | string | โฏ | _id of one of the racing tables |
| title | string | โฏ | Title / topic of the article to generate |
| opening_at | number | โฏ | UNIX timestamp (ms) |
| closing_at | number | UNIX timestamp (ms) |
Response
202 Accepted
Error handling
| Description | Status code | Status |
|---|---|---|
| Invalid request body | 422 | Unprocessable Entity |
race_id not found in any racing table |
400 | Bad Request |
| Race has no finishing order (void / cancelled) | 422 | Unprocessable Entity |
| Internal server error | 500 | Internal Server Error |
Processing flow
- Receive the request
- Verify
race_idexists in one of the three race collections - Verify the race result has a finishing order (not void / cancelled)
- Create the article in
generatingstate with an emptybody_html - Send
{article_id, race_id, racing_type, title, race_result}to thearticle-generationqueue - Return 202 Accepted with the new
id - (async) article_generation analyses the race, writes the article and sends it to
article-builder - (async) article_builder builds
body_html, stores it and movesstatustodraft
flowchart TD
Start([POST /v1/articles/generation]) --> RaceCheck[Check race_id exists]
RaceCheck -->|not found| E400[400 Bad Request]
RaceCheck -->|OK| VoidCheck[Check finishing order exists]
VoidCheck -->|none| E422[422 Unprocessable Entity]
VoidCheck -->|OK| SaveArticle[Save article as generating]
SaveArticle --> SendSQS[Send to article-generation queue]
SendSQS --> Success[202 Accepted]
SendSQS -.->|async| Generate[article_generation analyses + writes]
Generate -.-> Build[article_builder builds HTML and stores it]
Build -.-> Draft[status becomes draft]
Generate -.->|failed on final attempt| Failed[status becomes failed]
POST /v1/articles/{article_id}/regeneration
Overview
Rebuilds an article that failed to generate (status=failed), optionally with extra instructions.
Regenerating with the same input fails for the same reason, so an operator can supply writing
instructions (custom_prompt). If omitted, the previous instructions are reused, so repeatedly
triggering regeneration from the list view does not lose them.
Only status=failed articles are eligible: regenerating a draft or published article would
overwrite reviewed content.
Request and Response
Headers
Request
Content-Type: application/json
URI
| Path parameter | Type | Notes |
|---|---|---|
| article_id | string | article._id |
Request body
| Field | Type | Required | Notes |
|---|---|---|---|
| custom_prompt | string | Extra instructions from the operator. Omitted or empty reuses the previous instructions |
Response
202 Accepted
Error handling
| Description | Status code | Status |
|---|---|---|
| Article not found | 404 | Not Found |
Article status is not failed |
409 | Conflict |
race_id not found |
400 | Bad Request |
| Race has no finishing order (void / cancelled) | 422 | Unprocessable Entity |
| Internal server error | 500 | Internal Server Error |
Processing flow
flowchart TD
Start([POST /v1/articles/article_id/regeneration]) --> Fetch[Fetch the article]
Fetch -->|not found| E404[404 Not Found]
Fetch -->|found| StatusCheck[Check status=failed]
StatusCheck -->|not failed| E409[409 Conflict]
StatusCheck -->|OK| VoidCheck[Check the race finishing order]
VoidCheck -->|none| E422[422 Unprocessable Entity]
VoidCheck -->|OK| Reset[Reset status to generating, store custom_prompt]
Reset --> SendSQS[Send to article-generation queue]
SendSQS --> Success[202 Accepted]
Status is reset before enqueueing
article_generation only processes articles with status=generating, so the status is updated
before the message is enqueued.
POST /v1/articles
Overview
Creates an article manually.
Request and Response
Headers
Request
Content-Type: application/json
Response
Content-Type: application/json
URI
Request body
{
"race_id": "string",
"title": "string",
"body_html": "string",
"status": "draft",
"opening_at": 1234567890000,
"closing_at": 1234567890000
}
| Field | Type | Required | Notes |
|---|---|---|---|
| race_id | string | โฏ | _id of one of the racing tables |
| title | string | โฏ | |
| body_html | string | โฏ | HTML |
| status | enum | draft / published. Defaults to draft. generating / failed are rejected |
|
| opening_at | number | โฏ | UNIX timestamp (ms) |
| closing_at | number | UNIX timestamp (ms) |
Response
201 Created โ returns the article schema.
Error handling
| Description | Status code | Status |
|---|---|---|
| Invalid request body | 422 | Unprocessable Entity |
status set to generating / failed |
400 | Bad Request |
race_id not found |
400 | Bad Request |
| Internal server error | 500 | Internal Server Error |
GET /v1/articles
Overview
Lists articles.
Request and Response
URI
Query parameters
| Parameter | Type | Required | Notes |
|---|---|---|---|
| race_id | string | Filter by race | |
| status | enum | Filter by generating / draft / published / failed |
|
| page | number | Page number. Default: 1 |
|
| size | number | Items per page. Default: 10 |
|
| order | enum | asc / desc (by updated_at). Default: desc |
Response
200 OK
{
"list": [
{
"id": "string",
"race_id": "string",
"title": "string",
"body_html": "string",
"status": "draft",
"opening_at": 1234567890000,
"closing_at": null,
"accuracy_test_data": null,
"generation_error": null,
"custom_prompt": null,
"created_at": 1234567890000,
"updated_at": 1234567890000
}
],
"current_page": 1,
"total_count": 10
}
Failed-generation list
The admin "failed generations" screen is backed by GET /v1/articles?status=failed.
GET /v1/articles/{article_id}
Overview
Fetches a single article.
URI
Response
200 OK โ returns the article schema.
Error handling
| Description | Status code | Status |
|---|---|---|
| Article not found | 404 | Not Found |
| Internal server error | 500 | Internal Server Error |
GET /v1/races/{race_id}/articles/{article_id}
Overview
Fetches an article scoped to a race. If the article's race_id does not match the path, 404 is
returned.
The former per-sport endpoints (/v1/auto_racings/... and friends) were consolidated here along with
the race API (/v1/races).
URI
| Path parameter | Type | Notes |
|---|---|---|
| race_id | string | The race _id |
| article_id | string | article._id |
Response
200 OK โ returns the article schema.
Error handling
| Description | Status code | Status |
|---|---|---|
| Article not found | 404 | Not Found |
The article's race_id does not match the path |
404 | Not Found |
| Internal server error | 500 | Internal Server Error |
flowchart TD
Start([GET /v1/races/race_id/articles/article_id]) --> Fetch[Look up in DB]
Fetch -->|not found| E404[404 Not Found]
Fetch -->|found| RaceCheck[Compare race_id]
RaceCheck -->|mismatch| E404_race[404 Not Found]
RaceCheck -->|match| Success[200 OK]
PUT /v1/articles/{article_id}
Overview
Updates an article.
Request body
{
"race_id": "string",
"title": "string",
"body_html": "string",
"status": "published",
"opening_at": 1234567890000,
"closing_at": 1234567890000,
"accuracy_test_data": {}
}
| Field | Type | Required | Notes |
|---|---|---|---|
| race_id | string | _id of one of the racing tables |
|
| title | string | ||
| body_html | string | HTML | |
| status | enum | draft / published. generating / failed are rejected |
|
| opening_at | number | UNIX timestamp (ms) | |
| closing_at | number | UNIX timestamp (ms) | |
| accuracy_test_data | object | Accuracy-test data. See the article table for its structure |
Fields left out (null) are not updated.
Update body_html and accuracy_test_data together
When the body is corrected mechanically (e.g. "wire-to-wire" โ "held on"), updating only
body_html leaves the accuracy_test_data read by the audit
(scripts/article_accuracy_test/audit.py) stale, so the rendered article and the
verification result disagree. Both can be updated in a single request.
Response
200 OK โ returns the updated article schema.
Error handling
| Description | Status code | Status |
|---|---|---|
| Invalid request body | 422 | Unprocessable Entity |
status set to generating / failed |
400 | Bad Request |
race_id given but the race does not exist |
400 | Bad Request |
| Article not found | 404 | Not Found |
| Internal server error | 500 | Internal Server Error |
DELETE /v1/articles/{article_id}
Overview
Soft-deletes an article.
URI
Response
204 No Content
Error handling
| Description | Status code | Status |
|---|---|---|
| Article not found | 404 | Not Found |
| Internal server error | 500 | Internal Server Error |