Skip to main content

Fake Blog Post and Comment API

MOCK DATA

Fake blog posts with markdown bodies plus a threaded comment tree whose every parentId resolves inside the page — with working tag, status and date filters.

Generated test data for fixtures and development

Base URL
/api/posts
Capabilities
2 routes Seedable
Last updated
July 30, 2026

GET /api/posts

Each record is an independent post: its own topic vocabulary, an exact wordCount, an excerpt derived from the body, and a date/metrics mask that matches its status.

Live requestRuns against the public API
No key required
GET/api/posts?tag=engineering&status=published&count=10&seed=42

Request parameters

Pin tags[0] AND the vocabulary the title, excerpt and body are drawn from. Omit for a mix of all six topics.

Pin status on every record, and therefore its whole date and metrics mask. Omitted, you get a weighted mix: published 60%, archived 15%, draft 15%, scheduled 10%.

Exact number of entries in tags. Omit for a per-post mix of 1–3 (not echoed in meta.params, because it varies per record).

Inclusive lower bound on wordCount.

Inclusive upper bound on wordCount. Must be ≥ minWords. The 160 ceiling is what keeps count=100 inside the 256 KiB response budget.

Maximum length of excerpt in UTF-16 code units, the trailing … included. The excerpt is always a word-boundary prefix of the body's plain text.

Earliest createdAt, inclusive (YYYY-MM-DD = midnight UTC, or a full ISO 8601 timestamp). Defaults to 180 days before the resolved 'to', clamped to 0001-01-01. Must be ≤ to.

Latest createdAt/updatedAt, inclusive, and the publish boundary: published/archived posts publish at or before it, scheduled posts after it. Defaults to the final millisecond of the current UTC day. Must be ≤ 9998-12-31.

Advanced response options7 options

How many records to generate (1–100).

Deterministic output: the same seed always returns the same records. Omit for random (the used seed is echoed in meta.seed).

Return only these fields (comma-separated). Mutually exclusive with 'exclude'.

Return all fields except these (comma-separated).

Response format: json envelope, ndjson (one record per line) or csv.

Pretty-print the JSON response.

Drop the envelope: return the raw array/object without data/meta wrapper.

Response

Example parameters are ready. Send the request to inspect the live response.

Route reference

Code samples Ready-to-copy requests in 4 languages
Choose a code sample language
curl "https://randomapi.dev/api/posts?tag=engineering&status=published&count=10&seed=42"
const res = await fetch("https://randomapi.dev/api/posts?tag=engineering&status=published&count=10&seed=42");
const { data, meta } = await res.json();
import requests

data = requests.get("https://randomapi.dev/api/posts?tag=engineering&status=published&count=10&seed=42").json()["data"]
$json = json_decode(file_get_contents(
  "https://randomapi.dev/api/posts?tag=engineering&status=published&count=10&seed=42"
), true);
$data = $json["data"];
Parameters Route-specific request inputs 8
tag enum

Pin tags[0] AND the vocabulary the title, excerpt and body are drawn from. Omit for a mix of all six topics.

allowed: engineering | design | product | security | data | career
example: tag=security
status enum

Pin status on every record, and therefore its whole date and metrics mask. Omitted, you get a weighted mix: published 60%, archived 15%, draft 15%, scheduled 10%.

allowed: draft | scheduled | published | archived
example: status=published
tagCount int

Exact number of entries in tags. Omit for a per-post mix of 1–3 (not echoed in meta.params, because it varies per record).

allowed: 1 – 3
example: tagCount=1
minWords int

Inclusive lower bound on wordCount.

default: 60
allowed: 20 – 160
example: minWords=120
maxWords int

Inclusive upper bound on wordCount. Must be ≥ minWords. The 160 ceiling is what keeps count=100 inside the 256 KiB response budget.

default: 140
allowed: 20 – 160
example: maxWords=160
excerptChars int

Maximum length of excerpt in UTF-16 code units, the trailing … included. The excerpt is always a word-boundary prefix of the body's plain text.

default: 160
allowed: 40 – 200
example: excerptChars=200
from date

Earliest createdAt, inclusive (YYYY-MM-DD = midnight UTC, or a full ISO 8601 timestamp). Defaults to 180 days before the resolved 'to', clamped to 0001-01-01. Must be ≤ to.

example: from=2026-01-01
to date

Latest createdAt/updatedAt, inclusive, and the publish boundary: published/archived posts publish at or before it, scheduled posts after it. Defaults to the final millisecond of the current UTC day. Must be ≤ 9998-12-31.

example: to=2026-03-31
Universal parameters Shared response and formatting options 7
count int

How many records to generate (1–100).

default: 10
allowed: 1 – 100
example: count=3
seed int

Deterministic output: the same seed always returns the same records. Omit for random (the used seed is echoed in meta.seed).

example: seed=42
fields list

Return only these fields (comma-separated). Mutually exclusive with 'exclude'.

example: fields=id,slug
exclude list

Return all fields except these (comma-separated).

example: exclude=coverImageUrl
format enum

Response format: json envelope, ndjson (one record per line) or csv.

default: json
allowed: json | ndjson | csv
example: format=csv
pretty boolean

Pretty-print the JSON response.

default: false
example: pretty=true
unwrap boolean

Drop the envelope: return the raw array/object without data/meta wrapper.

default: false
example: unwrap=true
Response schema Fields returned in each record 14
id string

'post_' + 12 hex characters + 2 hex of the record index, so ids are unique inside a response.

example: post_9f3a2c018d3104

slug string

ASCII kebab-case slug derived from the title, at most 60 characters, suffixed with the id's last 4 hex characters so it is unique inside a response.

example: notes-on-hardening-the-default-path-3104

title string

At most 80 characters, assembled from the topic's own noun phrases.

example: Notes on hardening the default path

excerpt string

Word-boundary prefix of the body's PARAGRAPH text (paragraphs joined by a single space; '## ' heading blocks and '- ' bullet blocks are skipped, because neither carries sentence punctuation and splicing them in produced run-together teasers), at most excerptChars UTF-16 code units, with a trailing … only when truncated. Never generated separately from the body.

example: We drew the threat model on one page. The team agreed to keep it simple. Hardening the default saved every later argument. The exploit chain was three boring…

body string

CommonMark subset: blank-line-separated paragraphs, at most one '## ' subheading and at most one '- ' bullet list. A subheading needs a wordCount of at least 60 and a bullet list at least 85, so short bodies are plain paragraphs. No code fences, tables, links, images, emphasis or inline code — use /api/markdown for those.

example: We drew the threat model on one page. The team agreed to keep it simple. Hardening the default saved every later argument. The exploit chain was three boring steps. ## Hardening steps - rotate every long lived credential - fix the boring vulnerability first - log what the sandbox blocks The vulnerability had been open for months. Least privilege broke two internal tools first. We wrote it down so the next person could follow. It took longer than we expected. Rotation is easy once nothing depends on a secret.

tags string[]

1–3 labels. tags[0] is the topic id and the only vocabulary the title and body draw from; the rest are that same topic's sub-labels. Posts are never cross-tagged.

example: ["security","appsec"]

author object

Nested object: name, username (the ASCII slug of the name as first.last) and avatarUrl (a /api/avatars/image URL on this platform).

example: {"name":"Maria Ortiz","username":"maria.ortiz","avatarUrl":"https://randomapi.dev/api/avatars/image?style=initials&seed=maria.ortiz&name=Maria%20Ortiz"}

status string

draft | scheduled | published | archived.

example: published

createdAt string (ISO 8601)

Sampled inside slot i of 100 equal slots across [from, to], so records come oldest-first and record i depends only on the seed, the resolved params and i — never on count.

example: 2026-02-11T08:14:02.311Z

publishedAt string (ISO 8601) nullable

null for draft. published/archived: inside [createdAt, to]. scheduled: 1–30 days after to (clamped to 9999-12-31T23:59:59.999Z).

example: 2026-02-12T09:00:00.000Z

updatedAt string (ISO 8601)

published/archived: inside [publishedAt, to]. draft/scheduled: inside [createdAt, to]. Never before createdAt.

example: 2026-03-02T16:45:10.004Z

wordCount integer

Exactly the number of whitespace-separated tokens in body containing at least one ASCII letter — inside [minWords, maxWords]. The '##' and '-' markers contain no letters, so they never count.

example: 85

metrics object

Nested object: views ≥ likes ≥ commentCount ≥ 0, all exactly 0 for draft and scheduled. commentCount is the post's own display counter — NOT the number of rows /api/posts/comments returns for that id.

example: {"views":18420,"likes":640,"commentCount":71}

coverImageUrl string nullable

A /api/images/placeholder URL labelled with the topic; null for roughly 1 post in 5 so card layouts have to handle the fallback.

example: https://randomapi.dev/api/images/placeholder?width=1200&height=630&text=security

Documented examples

Build-generated requests and complete responses
5
Ten published engineering posts
GET /api/posts?tag=engineering&status=published&count=10&seed=42
{
  "data": [
    {
      "id": "post_7a958016cfb600",
      "slug": "what-we-learned-about-tail-latency-b600",
      "title": "What we learned about tail latency",
      "excerpt": "Tail latency is the number that mattered here. The compiler flagged it long before we did. We deleted more code than we added. Concurrency made the simple path…",
      "body": "Tail latency is the number that mattered here. The compiler flagged it long before we did. We deleted more code than we added. Concurrency made the simple path much harder. The benchmark only lied when we wanted it to. The second attempt went much better. The profiler pointed straight at the hot loop. We read the notes from last year first. Refactoring in small steps kept the suite green.\n\n## Numbers that mattered\n\n- keep the benchmark in the suite\n- write the invariant down\n\nNobody wanted to own this at first. We kept the old path around for a month. The numbers agreed with the hallway conversation. One invariant explained three separate bugs. Concurrency was fine until the retry loop arrived. One allocation per request adds up quickly. We shipped the refactor already.",
      "tags": [
        "engineering",
        "refactoring"
      ],
      "author": {
        "name": "Kaylie Gorczany-Considine",
        "username": "kaylie.gorczanyconsidine",
        "avatarUrl": "https://randomapi.dev/api/avatars/image?style=initials&seed=kaylie.gorczanyconsidine&name=Kaylie%20Gorczany-Considine"
      },
      "status": "published",
      "createdAt": "2026-02-01T12:37:23.646Z",
      "publishedAt": "2026-05-06T10:14:33.048Z",
      "updatedAt": "2026-06-30T03:12:43.221Z",
      "wordCount": 131,
      "metrics": {
        "views": 90,
        "likes": 5,
        "commentCount": 0
      },
      "coverImageUrl": "https://randomapi.dev/api/images/placeholder?width=1200&height=630&text=engineering"
    },
    {
      "id": "post_0078f248abec01",
      "slug": "a-benchmark-that-lies-without-the-guesswork-ec01",
      "title": "A benchmark that lies without the guesswork",
      "excerpt": "The compiler caught what review missed. Concurrency made the simple path much harder. Our benchmark numbers moved by a third. Refactoring in small steps kept…",
      "body": "The compiler caught what review missed. Concurrency made the simple path much harder. Our benchmark numbers moved by a third. Refactoring in small steps kept the suite green. The benchmark only lied when we wanted it to. We read the notes from last year first.\n\n## The regression story\n\nThe team agreed to keep it simple. The numbers agreed with the hallway conversation.\n\nThe second attempt went much better. Two people carried most of the work. The fix was smaller than the argument about it. One invariant explained three separate bugs. We shipped the refactor. Nobody wanted to own this at first. Half the work was deciding what not to do. The compiler flagged it long before we did. Concurrency was fine until the retry loop arrived.",
      "tags": [
        "engineering",
        "refactoring",
        "code-review"
      ],
    …
    "generatedAt": "2026-07-30T15:14:08.000Z"
  }
}
Drafts with no publish date
GET /api/posts?status=draft&count=5&seed=7
{
  "data": [
    {
      "id": "post_ad92c7e2436f00",
      "slug": "positioning-in-a-crowded-market-without-the-guesswork-6f00",
      "title": "Positioning in a crowded market without the guesswork",
      "excerpt": "Churn dropped once we fixed the boring parts. Discovery saved us a quarter of wasted effort. One afternoon of reading saved a week of typing. The second…",
      "body": "Churn dropped once we fixed the boring parts. Discovery saved us a quarter of wasted effort. One afternoon of reading saved a week of typing. The second attempt went much better.\n\n## What we cut\n\nThe team agreed to keep it simple. Onboarding lost people at the second screen. The roadmap held. We wrote the positioning down before anything else. We kept a short list of open questions.",
      "tags": [
        "product"
      ],
      "author": {
        "name": "Bell Treutel",
        "username": "bell.treutel",
        "avatarUrl": "https://randomapi.dev/api/avatars/image?style=initials&seed=bell.treutel&name=Bell%20Treutel"
      },
      "status": "draft",
      "createdAt": "2026-02-01T01:28:50.785Z",
      "publishedAt": null,
      "updatedAt": "2026-02-08T09:06:49.920Z",
      "wordCount": 67,
      "metrics": {
        "views": 0,
        "likes": 0,
        "commentCount": 0
      },
      "coverImageUrl": "https://randomapi.dev/api/images/placeholder?width=1200&height=630&text=product"
    },
    {
      "id": "post_995c0ed5cab801",
      "slug": "a-closer-look-at-one-quiet-vulnerability-b801",
      "title": "A closer look at one quiet vulnerability",
      "excerpt": "The threat model helped. We wrote it down so the next person could follow. The result was boring in the best way. Hardening the defaults removed most of the…",
      "body": "The threat model helped. We wrote it down so the next person could follow. The result was boring in the best way. Hardening the defaults removed most of the guesswork. It took longer than we expected.\n\n## What we found\n\nThe threat model told us what to ignore. One privilege boundary was doing all the work. Rotation is easy once nothing depends on a secret. We kept a short list of open questions.\n\nThe rollout was quiet and that was the goal. We kept the old path around for a month. The sandbox held already.",
      "tags": [
        "security",
        "appsec"
      ],
      "author": {
        "name": "Jermaine Schumm",
    …
    "generatedAt": "2026-07-30T15:14:08.000Z"
  }
}
Scheduled posts for a pinned June window
GET /api/posts?status=scheduled&from=2026-06-01&to=2026-06-30&count=5&seed=11
{
  "data": [
    {
      "id": "post_15f67bcd2c7600",
      "slug": "the-promotion-packet-in-practice-7600",
      "title": "The promotion packet in practice",
      "excerpt": "Half the work was deciding what not to do. Burnout looked like boredom for a while. One afternoon of reading saved a week of typing. The promotion came a…",
      "body": "Half the work was deciding what not to do. Burnout looked like boredom for a while. One afternoon of reading saved a week of typing.\n\n## What I would skip\n\nThe promotion came a quarter later than it should have. Burnout arrived slowly and then all at once. The apprenticeship needed a real project not a toy one. The plan changed twice before it stuck. Mentoring taught me more than it taught them. Feedback nobody can act on is just noise. The feedback landed in practice.",
      "tags": [
        "career"
      ],
      "author": {
        "name": "Gregg Haag",
        "username": "gregg.haag",
        "avatarUrl": "https://randomapi.dev/api/avatars/image?style=initials&seed=gregg.haag&name=Gregg%20Haag"
      },
      "status": "scheduled",
      "createdAt": "2026-06-01T05:08:10.674Z",
      "publishedAt": "2026-07-29T00:00:00.000Z",
      "updatedAt": "2026-06-04T08:37:08.833Z",
      "wordCount": 85,
      "metrics": {
        "views": 0,
        "likes": 0,
        "commentCount": 0
      },
      "coverImageUrl": "https://randomapi.dev/api/images/placeholder?width=1200&height=630&text=career"
    },
    {
      "id": "post_8f5fd3bb40c101",
      "slug": "a-closer-look-at-typography-that-survives-translation-c101",
      "title": "A closer look at typography that survives translation",
      "excerpt": "The plan changed twice before it stuck. The affordance was there but nobody saw it. The second attempt went much better. The palette survived a second review.…",
      "body": "The plan changed twice before it stuck. The affordance was there but nobody saw it.\n\n## What the prototype showed\n\nThe second attempt went much better. The palette survived a second review. Iconography needs a second look at the smallest size. We cut the palette in half.\n\nWe wrote it down so the next person could follow. The palette shrank everywhere.",
      "tags": [
        "design",
        "accessibility",
        "typography"
      ],
      "author": {
    …
    "generatedAt": "2026-07-30T15:14:08.000Z"
  }
}
Long-form security posts as CSV
GET /api/posts?tag=security&minWords=120&maxWords=160&excerptChars=200&count=20&format=csv
id,slug,title,excerpt,body,tags,author.name,author.username,author.avatarUrl,status,createdAt,publishedAt,updatedAt,wordCount,metrics.views,metrics.likes,metrics.commentCount,coverImageUrl
post_7a958016cfb600,what-we-learned-about-hardening-the-default-path-b600,What we learned about hardening the default path,Credential sprawl was the real problem. The exploit chain was three boring steps. Hardening the defaults removed most of the guesswork. Hardening the default saved every later argument. We rotated…,"Credential sprawl was the real problem. The exploit chain was three boring steps. Hardening the defaults removed most of the guesswork. Hardening the default saved every later argument. We rotated every credential in one afternoon. The second attempt went much better. The sandbox held but the logging did not. We read the notes from last year first. We drew the threat model on one page. The sandbox held since then.

## What we would do sooner

- draw the threat model on one page
- rotate every long lived credential

The numbers agreed with the hallway conversation. We shipped it on a quiet friday. The team agreed to keep it simple. The threat model told us what to ignore. Two people carried most of the work. The vulnerability had been open for months. Rotation is easy once nothing depends on a secret. Small steps beat one large leap here. We wrote it down so the next person could follow.","[""security"",""appsec""]",Kaylie Gorczany-Considine,kaylie.gorczanyconsidine,https://randomapi.dev/api/avatars/image?style=initials&seed=kaylie.gorczanyconsidine&name=Kaylie%20Gorczany-Considine,draft,2026-02-02T06:48:02.245Z,,2026-04-08T06:00:09.104Z,156,0,0,0,https://randomapi.dev/api/images/placeholder?width=1200&height=630&text=security
post_0078f248abec01,credential-sprawl-without-the-guesswork-ec01,Credential sprawl without the guesswork,We rotated every credential in one afternoon. Hardening the default saved every later argument. Credential sprawl was the real problem. The exploit needed no clever trick at all. Hardening the…,"We rotated every credential in one afternoon. Hardening the default saved every later argument. Credential sprawl was the real problem. The exploit needed no clever trick at all. Hardening the defaults removed most of the guesswork. We kept the old path around for a month.

## Hardening steps

We shipped it on a quiet friday. The numbers agreed with the hallway conversation. The second attempt went much better. The plan changed twice before it stuck. We asked for help earlier than usual. The threat model helped.

The sandbox held. The fix was smaller than the argument about it. We read the notes from last year first. The sandbox held but the logging did not. The exploit chain was three boring steps. Least privilege broke two internal tools first. Rotation is easy once nothing depends on a secret. Two people carried most of the work. We wrote it down so the next person could follow.","[""security"",""appsec"",""threat-modeling""]",Waino Marvin,waino.marvin,https://randomapi.dev/api/avatars/image?style=initials&seed=waino.marvin&name=Waino%20Marvin,published,2026-02-04T06:48:27.010Z,2026-05-28T09:30:42.731Z,2026-06-06T22:19:11.100Z,153,33238,2573,7,https://randomapi.dev/api/images/placeholder?width=1200&height=630&text=security
post_d15790902c0502,notes-on-an-exploit-chain-we-missed-0502,Notes on an exploit chain we missed,We kept a short list of open questions. Least privilege broke two internal tools first. The threat model helped. Hardening the default saved every later argument. The sandbox held but the logging did…,"We kept a short list of open questions. Least privilege broke two internal tools first. The threat model helped.

Hardening the default saved every later argument. The sandbox held but the logging did not. We kept the old path around for a month. We shipped it on a quiet friday. The vulnerability had been open for months. The threat model told us what to ignore. The sandbox held. Rotation is easy once nothing depends on a secret. Credential sprawl was the real problem. It took longer than we expected.

The result was boring in the best way. The exploit chain was three boring steps. The rollout was quiet and that was the goal. Half the work was deciding what not to do. One privilege boundary was doing all the work. The fix was smaller than the argument about it. The numbers agreed with the hallway conversation. We rotated every credential in one afternoon. The threat model helped twice.","[""security"",""appsec""]",Hailee Streich,hailee.streich,https://randomapi.dev/api/avatars/image?style=initials&seed=hailee.streich&name=Hailee%20Streich,published,2026-02-05T15:33:44.197Z,2026-03-02T20:12:43.480Z,2026-04-08T03:50:55.057Z,158,177172,1158,53,https://randomapi.dev/api/images/placeholder?width=1200&height=630&text=security
post_2902fda52e1e03,rethinking-key-rotation-without-downtime-1e03,Rethinking key rotation without downtime,Least privilege broke two internal tools first. The threat model helped. The exploit chain was three boring steps. One afternoon of reading saved a week of typing. Hardening the default saved every…,"Least privilege broke two internal tools first. The threat model helped.

## Hardening steps

- give each job the least privilege it needs
- draw the threat model on one page
- log what the sandbox blocks

The exploit chain was three boring steps. One afternoon of reading saved a week of typing. Hardening the default saved every later argument. The second attempt went much better. The plan changed twice before it stuck.

We shipped it on a quiet friday. It took longer than we expected. Small steps beat one large leap here. The vulnerability had been open for months. Rotation is easy once nothing depends on a secret. The sandbox held but the logging did not. We rotated every credential in one afternoon. Nobody wanted to own this at first. Credential sprawl was the real problem. The sandbox held for now.","[""security"",""secrets"",""threat-modeling""]",Otis Hodkiewicz,otis.hodkiewicz,https://randomapi.dev/api/avatars/image?style=initials&seed=otis.hodkiewicz&name=Otis%20Hodkiewicz,published,2026-02-07T19:31:29.204Z,2026-06-23T20:36:51.146Z,2026-07-22T01:46:11.425Z,138,26791,1320,0,https://randomapi.dev/api/images/placeholder?width=1200&height=630&text=security
post_db640beed4d804,a-closer-look-at-sandbox-escapes-d804,A closer look at sandbox escapes,The exploit chain was three boring steps. Hardening the defaults removed most of the guesswork. Rotation is easy once nothing depends on a secret. Hardening the default saved every later argument.…,"The exploit chain was three boring steps. Hardening the defaults removed most of the guesswork. Rotation is easy once nothing depends on a secret. Hardening the default saved every later argument. Credential sprawl was the real problem. One privilege boundary was doing all the work. It took longer than we expected. Small steps beat one large leap here. The threat model helped.

The rollout was quiet and that was the goal. The sandbox held for now.

Two people carried most of the work. We wrote it down so the next person could follow. The fix was smaller than the argument about it. We read the notes from last year first. The sandbox held but the logging did not. The vulnerability had been open for months. The threat model told us what to ignore. Half the work was deciding what not to do.","[""security"",""secrets"",""threat-modeling""]",Melisa Funk,melisa.funk,https://randomapi.dev/api/avatars/image?style=initials&seed=melisa.funk&name=Melisa%20Funk,draft,2026-02-09T05:44:46.707Z,,2026-04-03T02:16:50.760Z,142,0,0,0,https://randomapi.dev/api/images/placeholder?width=1200&height=630&text=security
post_066407ec74cc05,key-rotation-without-downtime-in-practice-cc05,Key rotation without downtime in practice,Hardening the default saved every later argument. The threat model helped. Rotation is easy once nothing depends on a secret. The rollout was quiet and that was the goal. Two people carried most of…,"Hardening the default saved every later argument. The threat model helped. Rotation is easy once nothing depends on a secret. The rollout was quiet and that was the goal. Two people carried most of the work. We rotated every credential in one afternoon. The vulnerability had been open for months. The sandbox held but the logging did not. It took longer than we expected.

## Hardening steps
    …

The threat model told us what to ignore. One privilege boundary was doing all the work. Credential sprawl was the real problem. The sandbox held in practice.","[""security""]",Susan Doyle,susan.doyle,https://randomapi.dev/api/avatars/image?style=initials&seed=susan.doyle&name=Susan%20Doyle,published,2026-03-08T18:27:21.752Z,2026-04-16T18:04:25.579Z,2026-05-12T21:08:10.317Z,123,157871,8299,2879,
Short single-tag career posts
GET /api/posts?tag=career&minWords=20&maxWords=40&tagCount=1&count=8&seed=3
{
  "data": [
    {
      "id": "post_5300fbb5997d00",
      "slug": "notes-on-interviewing-from-the-other-side-7d00",
      "title": "Notes on interviewing from the other side",
      "excerpt": "The promotion came a quarter later than it should have. We wrote it down so the next person could follow. The apprenticeship needed a real project not a toy…",
      "body": "The promotion came a quarter later than it should have.\n\nWe wrote it down so the next person could follow.\n\nThe apprenticeship needed a real project not a toy one.",
      "tags": [
        "career"
      ],
      "author": {
        "name": "Joana Marvin",
        "username": "joana.marvin",
        "avatarUrl": "https://randomapi.dev/api/avatars/image?style=initials&seed=joana.marvin&name=Joana%20Marvin"
      },
      "status": "published",
      "createdAt": "2026-02-02T12:42:37.628Z",
      "publishedAt": "2026-03-26T06:19:36.512Z",
      "updatedAt": "2026-07-01T23:44:11.525Z",
      "wordCount": 30,
      "metrics": {
        "views": 34751,
        "likes": 2835,
        "commentCount": 281
      },
      "coverImageUrl": "https://randomapi.dev/api/images/placeholder?width=1200&height=630&text=career"
    },
    {
      "id": "post_ff829ddf9c8601",
      "slug": "mentoring-without-a-template-in-practice-8601",
      "title": "Mentoring without a template in practice",
      "excerpt": "Feedback nobody can act on is just noise. Mentoring worked best with a standing half hour. The feedback landed twice. Burnout looked like boredom for a while.…",
      "body": "Feedback nobody can act on is just noise. Mentoring worked best with a standing half hour. The feedback landed twice.\n\nBurnout looked like boredom for a while. The mentoring helped already.",
      "tags": [
        "career"
      ],
      "author": {
        "name": "Molly Jacobi",
        "username": "molly.jacobi",
    …
    "generatedAt": "2026-07-30T15:14:08.000Z"
  }
}

GET /api/posts/comments

One pool of 100 rows built parent-first; count returns a prefix of it. Row 0 is always a root, and every non-null parentId names a row printed earlier in the same response.

Live requestRuns against the public API
No key required
GET/api/posts/comments?postId=post_9f3a2c018d3104&count=20&seed=42

Request parameters

Pin postId on every row so you can attach a comment page to a post id you already hold. Letters, digits, '.', '_', ':' and '-' only. Supplying it resolves postCount to 1.

Number of distinct synthetic post ids the pool is spread across. Resolves to 3, or to 1 when postId is supplied (in which case an explicit value is ignored with a warning). The resolved value is echoed in meta.params.

Maximum depth of the reply tree. 0 gives a flat list: every parentId null, every depth 0.

Pin the moderation status on every row. Omitted, you get published 75%, pending 15%, spam 10%. Status never affects tree structure. Note this enum differs from the post route's status.

Inclusive lower bound on wordCount.

Inclusive upper bound on wordCount. Must be ≥ minWords.

Earliest createdAt, inclusive (YYYY-MM-DD = midnight UTC, or a full ISO 8601 timestamp). Defaults to 90 days before the resolved 'to', clamped to 0001-01-01. Must be ≤ to.

Latest createdAt/updatedAt, inclusive. Defaults to the final millisecond of the current UTC day. Must be ≤ 9998-12-31.

Advanced response options7 options

Limit the number of returned records (1–100). Defaults to 20.

Deterministic output: the same seed always returns the same records. Omit for random (the used seed is echoed in meta.seed).

Return only these fields (comma-separated). Mutually exclusive with 'exclude'.

Return all fields except these (comma-separated).

Response format: json envelope, ndjson (one record per line) or csv.

Pretty-print the JSON response.

Drop the envelope: return the raw array/object without data/meta wrapper.

Response

Example parameters are ready. Send the request to inspect the live response.

Route reference

Code samples Ready-to-copy requests in 4 languages
Choose a code sample language
curl "https://randomapi.dev/api/posts/comments?postId=post_9f3a2c018d3104&count=20&seed=42"
const res = await fetch("https://randomapi.dev/api/posts/comments?postId=post_9f3a2c018d3104&count=20&seed=42");
const { data, meta } = await res.json();
import requests

data = requests.get("https://randomapi.dev/api/posts/comments?postId=post_9f3a2c018d3104&count=20&seed=42").json()["data"]
$json = json_decode(file_get_contents(
  "https://randomapi.dev/api/posts/comments?postId=post_9f3a2c018d3104&count=20&seed=42"
), true);
$data = $json["data"];
Parameters Route-specific request inputs 8
postId string

Pin postId on every row so you can attach a comment page to a post id you already hold. Letters, digits, '.', '_', ':' and '-' only. Supplying it resolves postCount to 1.

allowed: 1 – 64
example: postId=post_9f3a2c018d3104
postCount int

Number of distinct synthetic post ids the pool is spread across. Resolves to 3, or to 1 when postId is supplied (in which case an explicit value is ignored with a warning). The resolved value is echoed in meta.params.

allowed: 1 – 10
example: postCount=6
maxDepth int

Maximum depth of the reply tree. 0 gives a flat list: every parentId null, every depth 0.

default: 2
allowed: 0 – 4
example: maxDepth=4
status enum

Pin the moderation status on every row. Omitted, you get published 75%, pending 15%, spam 10%. Status never affects tree structure. Note this enum differs from the post route's status.

allowed: published | pending | spam
example: status=pending
minWords int

Inclusive lower bound on wordCount.

default: 8
allowed: 3 – 60
example: minWords=20
maxWords int

Inclusive upper bound on wordCount. Must be ≥ minWords.

default: 30
allowed: 3 – 60
example: maxWords=60
from date

Earliest createdAt, inclusive (YYYY-MM-DD = midnight UTC, or a full ISO 8601 timestamp). Defaults to 90 days before the resolved 'to', clamped to 0001-01-01. Must be ≤ to.

example: from=2026-01-01
to date

Latest createdAt/updatedAt, inclusive. Defaults to the final millisecond of the current UTC day. Must be ≤ 9998-12-31.

example: to=2026-01-31
Universal parameters Shared response and formatting options 7
count int

Limit the number of returned records (1–100). Defaults to 20.

default: 20
allowed: 1 – 100
example: count=3
seed int

Deterministic output: the same seed always returns the same records. Omit for random (the used seed is echoed in meta.seed).

example: seed=42
fields list

Return only these fields (comma-separated). Mutually exclusive with 'exclude'.

example: fields=id,postId
exclude list

Return all fields except these (comma-separated).

example: exclude=status
format enum

Response format: json envelope, ndjson (one record per line) or csv.

default: json
allowed: json | ndjson | csv
example: format=csv
pretty boolean

Pretty-print the JSON response.

default: false
example: pretty=true
unwrap boolean

Drop the envelope: return the raw array/object without data/meta wrapper.

default: false
example: unwrap=true
Response schema Fields returned in each record 12
id string

'comment_' + 12 hex characters + 2 hex of the row index, so ids are unique inside a response.

example: comment_4c81be02a71d05

postId string

Your postId, or one of postCount synthetic 'post_…' ids. A reply always shares its parent's postId.

example: post_9f3a2c018d3104

parentId string nullable

null for a root comment; otherwise the id of a row printed EARLIER in the same response.

example: comment_4c81be02a71d02

depth integer

0 for a root, otherwise parent.depth + 1; never above maxDepth.

example: 1

author object

Nested object: name, email (<first.last>@example.com/.net/.org — RFC 2606 §3 reserved, so it can never deliver mail) and avatarUrl (a /api/avatars/image URL on this platform).

example: {"name":"Jonas Weber","email":"jonas.weber@example.net","avatarUrl":"https://randomapi.dev/api/avatars/image?style=initials&seed=jonas.weber&name=Jonas%20Weber"}

body string

Plain text — no markdown at all, because a comment box is not a CMS editor.

example: This matches what we saw in staging. We hit the same thing again.

wordCount integer

Whitespace-separated tokens in body containing at least one ASCII letter; inside [minWords, maxWords].

example: 13

createdAt string (ISO 8601)

Sampled inside slot i of 100 equal slots across [from, to], so rows are chronological AND parent-first at once — a reply is never dated before the comment it answers.

example: 2026-01-18T11:02:44.918Z

updatedAt string (ISO 8601)

Exactly createdAt when edited is false; strictly later (and ≤ to) when it is true.

example: 2026-01-18T11:02:44.918Z

edited boolean

True for roughly 1 row in 6 — true exactly when updatedAt > createdAt.

example: false

likes integer

0–999, skewed low the way real comment sections are.

example: 4

status string

published | pending | spam. Note this enum differs from the post route's status.

example: published

Documented examples

Build-generated requests and complete responses
5
Twenty threaded comments for one post
GET /api/posts/comments?postId=post_9f3a2c018d3104&count=20&seed=42
{
  "data": [
    {
      "id": "comment_a958016cfb6a00",
      "postId": "post_9f3a2c018d3104",
      "parentId": null,
      "depth": 0,
      "author": {
        "name": "Dorian Bahringer",
        "email": "dorian.bahringer@example.net",
        "avatarUrl": "https://randomapi.dev/api/avatars/image?style=initials&seed=dorian.bahringer&name=Dorian%20Bahringer"
      },
      "body": "Worth a follow up post. Adding this to my notes. The tooling part matters more than people admit. I would love to see the raw logs.",
      "wordCount": 26,
      "createdAt": "2026-05-02T07:54:44.633Z",
      "updatedAt": "2026-05-02T07:54:44.633Z",
      "edited": false,
      "likes": 642,
      "status": "pending"
    },
    {
      "id": "comment_d40fb15863c101",
      "postId": "post_9f3a2c018d3104",
      "parentId": "comment_a958016cfb6a00",
      "depth": 1,
      "author": {
        "name": "Pauline Dare",
        "email": "pauline.dare@example.com",
        "avatarUrl": "https://randomapi.dev/api/avatars/image?style=initials&seed=pauline.dare&name=Pauline%20Dare"
      },
      "body": "The second half is the useful part. Sharing the raw numbers would help a lot. Still thinking about this. This is the clearest write up I have read on it.",
      "wordCount": 30,
      "createdAt": "2026-05-03T15:57:12.582Z",
      "updatedAt": "2026-05-03T15:57:12.582Z",
      "edited": false,
      "likes": 273,
      "status": "published"
    },
    {
      "id": "comment_0b56b87c243102",
    …
    "generatedAt": "2026-07-30T15:14:08.000Z"
  }
}
A flat comment list with no replies
GET /api/posts/comments?maxDepth=0&count=25&seed=8
{
  "data": [
    {
      "id": "comment_c8e6c2ed0e0e00",
      "postId": "post_7ed05d64418b02",
      "parentId": null,
      "depth": 0,
      "author": {
        "name": "Etha Weissnat",
        "email": "etha.weissnat@example.org",
        "avatarUrl": "https://randomapi.dev/api/avatars/image?style=initials&seed=etha.weissnat&name=Etha%20Weissnat"
      },
      "body": "We ended up going the other way. The tooling part matters more than people admit. We solved it differently.",
      "wordCount": 19,
      "createdAt": "2026-05-02T04:38:16.387Z",
      "updatedAt": "2026-05-02T04:38:16.387Z",
      "edited": false,
      "likes": 523,
      "status": "published"
    },
    {
      "id": "comment_8dd634067d4501",
      "postId": "post_98125864a0ce01",
      "parentId": null,
      "depth": 0,
      "author": {
        "name": "General Schimmel",
        "email": "general.schimmel@example.org",
        "avatarUrl": "https://randomapi.dev/api/avatars/image?style=initials&seed=general.schimmel&name=General%20Schimmel"
      },
      "body": "We ran into the same wall. I am not sure I agree with the second point. I read this twice everywhere instead.",
      "wordCount": 22,
      "createdAt": "2026-05-03T11:47:16.013Z",
      "updatedAt": "2026-05-03T11:47:16.013Z",
      "edited": false,
      "likes": 851,
      "status": "pending"
    },
    {
      "id": "comment_ef251140dc7702",
    …
    "generatedAt": "2026-07-30T15:14:08.000Z"
  }
}
Deep reply chains across five posts
GET /api/posts/comments?maxDepth=4&postCount=5&count=100&seed=13
{
  "data": [
    {
      "id": "comment_f04daec2f14600",
      "postId": "post_1093aaf880df03",
      "parentId": null,
      "depth": 0,
      "author": {
        "name": "Judith Breitenberg",
        "email": "judith.breitenberg@example.com",
        "avatarUrl": "https://randomapi.dev/api/avatars/image?style=initials&seed=judith.breitenberg&name=Judith%20Breitenberg"
      },
      "body": "We solved it differently. Noted and shared. Bookmarking this for the next time it comes up.",
      "wordCount": 16,
      "createdAt": "2026-05-02T00:51:42.418Z",
      "updatedAt": "2026-05-02T00:51:42.418Z",
      "edited": false,
      "likes": 761,
      "status": "pending"
    },
    {
      "id": "comment_d09b0c7674d001",
      "postId": "post_1093aaf880df03",
      "parentId": "comment_f04daec2f14600",
      "depth": 1,
      "author": {
        "name": "Elisabeth Kris",
        "email": "elisabeth.kris@example.org",
        "avatarUrl": "https://randomapi.dev/api/avatars/image?style=initials&seed=elisabeth.kris&name=Elisabeth%20Kris"
      },
      "body": "Curious whether it held up after a month. Noted and shared. I am not sure I agree with the second point.",
      "wordCount": 21,
      "createdAt": "2026-05-02T21:43:57.619Z",
      "updatedAt": "2026-05-02T21:43:57.619Z",
      "edited": false,
      "likes": 162,
      "status": "pending"
    },
    {
      "id": "comment_709dfdc7cd9d02",
    …
    "generatedAt": "2026-07-30T15:14:08.000Z"
  }
}
Spam queue for a moderation UI
GET /api/posts/comments?status=spam&count=15&seed=5
{
  "data": [
    {
      "id": "comment_f09c06aab35e00",
      "postId": "post_e26f9066eacd02",
      "parentId": null,
      "depth": 0,
      "author": {
        "name": "Craig Lemke",
        "email": "craig.lemke@example.org",
        "avatarUrl": "https://randomapi.dev/api/avatars/image?style=initials&seed=craig.lemke&name=Craig%20Lemke"
      },
      "body": "The measurement approach is the interesting part. Bookmarking this for the next time it comes up. Good timing on this one. We solved it differently.",
      "wordCount": 25,
      "createdAt": "2026-05-02T15:43:59.968Z",
      "updatedAt": "2026-05-02T15:43:59.968Z",
      "edited": false,
      "likes": 36,
      "status": "spam"
    },
    {
      "id": "comment_698bced3f90801",
      "postId": "post_e26f9066eacd02",
      "parentId": "comment_f09c06aab35e00",
      "depth": 1,
      "author": {
        "name": "Julia Russel",
        "email": "julia.russel@example.org",
        "avatarUrl": "https://randomapi.dev/api/avatars/image?style=initials&seed=julia.russel&name=Julia%20Russel"
      },
      "body": "I would love to see the raw logs. That is a good point.",
      "wordCount": 13,
      "createdAt": "2026-05-03T16:09:30.360Z",
      "updatedAt": "2026-05-03T16:09:30.360Z",
      "edited": false,
      "likes": 18,
      "status": "spam"
    },
    {
      "id": "comment_7c8f3238a8d902",
    …
    "generatedAt": "2026-07-30T15:14:08.000Z"
  }
}
Comments inside a pinned January window
GET /api/posts/comments?from=2026-01-01&to=2026-01-31&count=30&seed=6
{
  "data": [
    {
      "id": "comment_cbce5cf2258f00",
      "postId": "post_bee281fc10ed01",
      "parentId": null,
      "depth": 0,
      "author": {
        "name": "Alphonso Walsh",
        "email": "alphonso.walsh@example.org",
        "avatarUrl": "https://randomapi.dev/api/avatars/image?style=initials&seed=alphonso.walsh&name=Alphonso%20Walsh"
      },
      "body": "Worth a follow up post. We ran into the same wall. Sharing the raw numbers would help a lot.",
      "wordCount": 19,
      "createdAt": "2026-01-01T02:29:04.672Z",
      "updatedAt": "2026-01-01T02:29:04.672Z",
      "edited": false,
      "likes": 101,
      "status": "published"
    },
    {
      "id": "comment_3cbdf641ceae01",
      "postId": "post_bee281fc10ed01",
      "parentId": null,
      "depth": 0,
      "author": {
        "name": "Sonja Windler",
        "email": "sonja.windler@example.net",
        "avatarUrl": "https://randomapi.dev/api/avatars/image?style=initials&seed=sonja.windler&name=Sonja%20Windler"
      },
      "body": "Worth a follow up post. Bookmarking this for the next time it comes up.",
      "wordCount": 14,
      "createdAt": "2026-01-01T14:02:20.548Z",
      "updatedAt": "2026-01-01T14:02:20.548Z",
      "edited": false,
      "likes": 678,
      "status": "published"
    },
    {
      "id": "comment_bb5e49fc7ca002",
    …
    "generatedAt": "2026-07-30T15:14:08.000Z"
  }
}

About this API

Coverage & behavior

Two routes: CMS-shaped blog posts, and a threaded comment page for them.

The comment tree actually resolves

/api/posts/comments builds one pool of 100 rows parent-first — a reply's parent is always a row at a lower index — and count returns a prefix of that pool. Every prefix is therefore closed under parentId: row 0 is always a root, every non-null parentId names a row printed above it in the same response, depth is exactly parent.depth + 1, and a reply always inherits its parent's postId. count=1 is a valid tree; so is count=100.

maxDepth genuinely bounds the nesting: maxDepth=0 gives you a flat, one-level list (every parentId null, every depth 0), maxDepth=4 produces real reply chains four levels deep and never five.

Tag-coherent text, not lorem with a label

Each of the six topics — engineering, design, product, security, data, career — owns a disjoint vocabulary. ?tag=security pins tags[0] and the pool the title, excerpt and body are drawn from, so a security post contains security words (threat, exploit, credential, hardening, privilege, rotation…) and zero words from the other five topics' sets. Neutral connective vocabulary is shared, so the prose still reads like prose. Sub-labels in tags[1..] always belong to the same topic — posts are deliberately never cross-tagged, because a foreign tag would be a label the body never earns.

Bodies, excerpts and word counts that agree

minWords/maxWords bound wordCount (20–160), and wordCount is exactly the number of whitespace-separated tokens in body that contain an ASCII letter — the ## and - markers contain none, so they never count. The body is a deliberate CommonMark subset: blank-line-separated paragraphs, at most one ## subheading (from wordCount 60 up) and at most one - bullet list (from 85 up). No code fences, tables, links, images, emphasis or inline code; that is /api/markdown's job, where each of them is a separately honored flag.

No body repeats a sentence. Every clause pool holds far more distinct words than the 160-word ceiling, and a clause already used in a body is never chosen again while an unused one still fits the remaining budget — so even minWords=160&maxWords=160 gives you 160 words of non-repeating prose rather than the same paragraph twice.

excerpt is never generated separately from the body — it is a word-boundary prefix of the body's paragraph text (the paragraphs joined by single spaces), at most excerptChars UTF-16 code units, with a trailing only when it was actually truncated. ## headings and - bullets are skipped rather than spliced in: neither carries sentence punctuation, so including them ran a heading straight into the next sentence. That is the only non-ASCII character in the endpoint's own vocabulary: titles, slugs, bodies, usernames and comment emails are pure ASCII, and author names are ASCII-folded before a username, e-mail or slug is derived from them.

Statuses drive the dates and the metrics

status publishedAt updatedAt metrics
draft null inside [createdAt, to] all exactly 0
scheduled 1–30 days after to inside [createdAt, to] all exactly 0
published inside [createdAt, to] inside [publishedAt, to] views ≥ 1, views ≥ likes ≥ commentCount ≥ 0
archived inside [createdAt, to] inside [publishedAt, to] same as published

Omit status for a weighted mix (published 60 %, archived 15 %, draft 15 %, scheduled 10 %). likes and commentCount are derived by multiplying down from views, so the ordering is structural rather than lucky. metrics.commentCount is a display counter and is unrelated to how many rows /comments returns for that id.

The two routes' status enums are different on purpose: a post is draft | scheduled | published | archived, a comment is published | pending | spam (moderation states). A pending reply to a published comment is intentional — status never affects tree structure.

Determinism

Record i is dated inside slot i of 100 equal slots across [from, to], so records come out oldest-first and record i depends only on the seed, the resolved params and inever on count. The first three rows of ?seed=42&count=3 are byte-identical to the first three of ?seed=42&count=100 on both routes. Changing a param that a record consumes randomness for (tag, status, tagCount, minWords/maxWords) shifts that record's whole draw sequence, so its createdAt moves inside the window too — pin every param you care about, not just the seed.

Both routes read the clock only to default to (the final millisecond of the current UTC day), so a pinned seed drifts once per UTC day; the resolved window is echoed in meta.params. Pin from and to for a frozen fixture. A window narrower than 100 ms has fewer milliseconds than slots, so slots collapse: timestamps become non-decreasing instead of strictly increasing, and a warning says so.

What this is not

  • Not correlated with /api/posts. /comments mints its own synthetic post_… ids; they are not the ids /api/posts returns for the same seed, and row 0's author is not post 0's author. That independence is deliberate work, not luck: the platform hands both routes the same seeded generators for record 0, so the comment route steps them past the prefix the post route consumes before it mints anything. The supported mechanism for tying the two together is ?postId= — pass any post id you already hold and every row is pinned to it (and postCount is then ignored, with a warning).
  • No lookup route, and therefore no 404s. A mock lookup would have to invent a record for any id you asked for, which is exactly the silently-wrong-record failure this platform refuses. Fetch a page and use the ids it hands you.
  • No replyCount on comments. It could only be counted over the full 100-row pool, so a smaller count would claim more replies than it shows. Rows are self-describing instead.
  • No readingMinutes. With bodies capped at 160 words it would always be 1. Compute it from wordCount if you need it.
  • No ?locale=. The topic pools are English; a locale param would translate author names only and leave every title and body in English — a half-honored param.
  • Not a paged "latest posts" feed. Records are oldest-first inside the resolved window; /api/pagination covers cursors and page envelopes.

How it differs from its neighbours

/api/conversations is private chat threads with participants and unread counts. /api/reviews is star-rated product opinions with sentiment-coherent text. /api/relational-seed?schema=blog emits a whole five-table blog schema for database seeding — with flat, one-level comment rows (post_id only, no parentId), pst_/cmt_ id prefixes and a /sql route of INSERT statements. This endpoint is the CMS article plus its public, genuinely threaded comment tree, which is why its ids use post_/comment_ instead — nobody should assume the two interchange.

CSV shape

format=csv flattens nested objects to dot-keys (author.name, author.email, metrics.views) and JSON-encodes arrays inside one cell (tags"[""security"",""appsec""]"). null renders as an empty cell, so status=draft&format=csv gives you empty publishedAt cells.

Use it for

  • Populate a blog index, article page and card grid — including the 1-in-5 posts with no cover image
  • Build a nested comment thread UI knowing every parentId resolves on the page you fetched
  • Test a moderation queue with ?status=spam and a flat one-level list with ?maxDepth=0
  • Exercise draft/scheduled/published/archived states with dates and counters that match the status

Frequently asked questions

Does every comment parentId resolve inside the response?

Yes. Rows are emitted parent-first, so any count returns a prefix that is closed under parentId: a root's parentId is null, and every reply names a row printed above it in the same response.

Can I get a flat, one-level comment list?

Yes — maxDepth=0 returns roots only: every parentId is null and every depth is 0.

Do these comments belong to the posts from /api/posts?

No. /api/posts/comments mints its own synthetic post_… ids, and they are not the ids /api/posts returns for the same seed — the comment route deliberately steps its generators past the prefix the post route consumes, so a shared seed cannot line the two up by accident. Pass postId= to attach a comment page to any post id you already hold.

How long are the post bodies, and can I get a full-length article?

minWords/maxWords bound wordCount between 20 and 160 words, so count=100 stays inside the platform's 256 KiB response budget. For a long multi-section document with code fences, tables and images, use /api/markdown.

How is this different from /api/conversations, /api/reviews and /api/relational-seed?

posts is a CMS article plus its public comment tree; conversations is private chat threads with unread counts; reviews is star-rated product opinions; relational-seed emits a whole blog schema for database seeding, with flat one-level comment rows and SQL inserts.

Are the comment author emails real addresses?

No — every one uses an RFC 2606 §3 reserved domain (example.com/.net/.org), which can never deliver mail.

Standards & references