sondahub

APIs / Social

Social

A social network: users, posts, comments, likes and follows — the GraphQL one.

Eight hundred users, four thousand posts, eight thousand comments and the likes and follows between them. Deeply related, which is what GraphQL is for: a user, their posts, each post’s comments and their authors in one query. 25,800 records in all.

Connect

Base URL
https://api.sondahub.com/v1/social
OpenAPI 3
https://api.sondahub.com/v1/social/openapi.json
GraphQL
https://api.sondahub.com/v1/social/graphql
WebSocket
wss://api.sondahub.com/v1/social/ws
SSE
https://api.sondahub.com/v1/social/events
The data
users.json, posts.json, comments.json, likes.json, follows.json

In Sonda: Import → From a URL with the OpenAPI address and the whole API lands as a project, one request per operation with example bodies. No keys, no headers to add. More on each protocol.

Writes here are simulated: POST, PUT, PATCH and DELETE are validated, run through the real logic and answered as a real server would — then forgotten. The answer carries _note and X-Sondahub-Write: simulated. A GET afterwards will not find what you wrote.
The API describes itself
curl https://api.sondahub.com/v1/social

Lists and filters

Every list answers { "data": [...], "meta": { "page", "limit", "total", "pages" } } with X-Total-Count and Link headers (next, prev, first, last). These options work on every collection and every nested route:

OptionMeaningExample
page, limitPaging, 1-based; limit 1–200, default 20. offset works too.?page=3&limit=50
sortComma list of fields, - for descending. Default id here.?sort=-id,id
field=valueEquals. Booleans as true/false, null for missing.?id=1
_ne _gt _gte _lt _lteNot equal and comparisons, on numbers, dates and strings.?id_gt=10
_likeContains, case-insensitive.?username_like=an
_inAny of a comma list.?id_in=1,2,3
_nulltrue: missing; false: present.?email_null=true
a.b=valueInside a JSON field, dotted.?media.kind=…
qSearch across the text fields.?q=alpine
fieldsOnly these fields back.?fields=id,username
expandEmbed related records.?expand=posts,comments

A name that is not a field answers 400 and lists the fields. Writes answer 422 with one line per problem, 404 for a missing id, 405 with an Allow header for a verb a route does not take.

users

Members. 800 records — the file.

FieldTypeNotes
idread-onlyintAssigned by the server. Seed records keep their ids across restarts; records you create continue after the seed.
created_atread-onlydatetimeWhen the record was created (ISO 8601, UTC).
updated_atread-onlydatetimeWhen the record last changed.
usernamerequiredstringUnique handle.
display_namerequiredstring
emailstring
biotext
avatar_urlstringA generated SVG served by the hub.
locationstring
websitestring
verifiedbool
privatebool
followers_countread-onlyint
following_countread-onlyint
posts_countread-onlyint
joined_atdatetime

Relations: posts → the posts whose author_id is this user; comments → the comments whose author_id is this user; likes → the likes whose user_id is this user; followers → the follows whose followee_id is this user; following → the follows whose follower_id is this user. Use ?expand=posts,comments,likes,followers,following to embed them, or the routes below.

Endpoints

GET/v1/social/usersA page, with every filter, sort, search, field and expand option below.
POST/v1/social/usersCreate one. Answers 201 with the record, its id and a Location header — simulated, nothing stored; 422 names each field that is wrong.
GET/v1/social/users/{id}One record, with an ETag; If-None-Match earns a 304.
PATCH/v1/social/users/{id}Change the fields you send.
PUT/v1/social/users/{id}Replace the record; required fields must all be there.
DELETE/v1/social/users/{id}Answers 200 with what would have been removed and the note (a real server’s 204 lives at /v1/utils/status/204).
GET/v1/social/users/{id}/postsIts posts, as a page with all the list options.
GET/v1/social/users/{id}/commentsIts comments, as a page with all the list options.
GET/v1/social/users/{id}/likesIts likes, as a page with all the list options.
GET/v1/social/users/{id}/followersIts follows, as a page with all the list options.
GET/v1/social/users/{id}/followingIts follows, as a page with all the list options.

Try it

List with a filter
curl "https://api.sondahub.com/v1/social/users?limit=3"
One record
curl https://api.sondahub.com/v1/social/users/1
Its posts
curl "https://api.sondahub.com/v1/social/users/1/posts?limit=5"
Create (simulated)
curl -X POST https://api.sondahub.com/v1/social/users \
  -H "Content-Type: application/json" \
  -d '{"username":"camila_fernandez","display_name":"A display name"}'
Change one field (simulated)
curl -X PATCH https://api.sondahub.com/v1/social/users/1 \
  -H "Content-Type: application/json" \
  -d '{"username":"Changed username"}'
Delete (simulated)
curl -X DELETE https://api.sondahub.com/v1/social/users/1

posts

What people write. hashtags is an array; filter with ?hashtags_like=coffee. 4,000 records — the file.

POST fills defaults (public, empty hashtags, now) and bumps the author’s posts_count.
FieldTypeNotes
idread-onlyintAssigned by the server. Seed records keep their ids across restarts; records you create continue after the seed.
created_atread-onlydatetimeWhen the record was created (ISO 8601, UTC).
updated_atread-onlydatetimeWhen the record last changed.
author_idrequiredint → users
bodyrequiredtext
hashtagsjson string[]
mediajson { kind, url, alt? }[]
visibilityenumpublic followers private
reply_to_idint → postsSet when the post is a reply.
likes_countread-onlyint
comments_countread-onlyint
reposts_countread-onlyint
languagestring
published_atdatetime
edited_atdatetime

Relations: author → one user through author_id; reply_to → one post through reply_to_id; comments → the comments whose post_id is this post; likes → the likes whose post_id is this post. Use ?expand=author,comments,likes,reply_to to embed them, or the routes below.

Endpoints

GET/v1/social/postsA page, with every filter, sort, search, field and expand option below.
POST/v1/social/postsCreate one. Answers 201 with the record, its id and a Location header — simulated, nothing stored; 422 names each field that is wrong.
GET/v1/social/posts/{id}One record, with an ETag; If-None-Match earns a 304.
PATCH/v1/social/posts/{id}Change the fields you send.
PUT/v1/social/posts/{id}Replace the record; required fields must all be there.
DELETE/v1/social/posts/{id}Answers 200 with what would have been removed and the note (a real server’s 204 lives at /v1/utils/status/204).
GET/v1/social/posts/{id}/authorThe user this record points at.
GET/v1/social/posts/{id}/commentsIts comments, as a page with all the list options.
GET/v1/social/posts/{id}/likesIts likes, as a page with all the list options.
GET/v1/social/posts/{id}/reply_toThe post this record points at.

Try it

List with a filter
curl "https://api.sondahub.com/v1/social/posts?visibility=followers&expand=author&limit=3"
One record
curl https://api.sondahub.com/v1/social/posts/1?expand=author
Its comments
curl "https://api.sondahub.com/v1/social/posts/1/comments?limit=5"
Create (simulated)
curl -X POST https://api.sondahub.com/v1/social/posts \
  -H "Content-Type: application/json" \
  -d '{"author_id":1,"body":"A body","visibility":"public","language":"en"}'
Change one field (simulated)
curl -X PATCH https://api.sondahub.com/v1/social/posts/1 \
  -H "Content-Type: application/json" \
  -d '{"visibility":"followers"}'
Delete (simulated)
curl -X DELETE https://api.sondahub.com/v1/social/posts/1

comments

Comments on posts; a comment can answer another comment through parent_id. 8,000 records — the file.

POST bumps the post’s comments_count; DELETE lowers it.
FieldTypeNotes
idread-onlyintAssigned by the server. Seed records keep their ids across restarts; records you create continue after the seed.
created_atread-onlydatetimeWhen the record was created (ISO 8601, UTC).
updated_atread-onlydatetimeWhen the record last changed.
post_idrequiredint → posts
author_idrequiredint → users
parent_idint → comments
bodyrequiredtext
likes_countread-onlyint
flaggedbool

Relations: post → one post through post_id; author → one user through author_id; parent → one comment through parent_id; replies → the comments whose parent_id is this comment. Use ?expand=post,author,parent,replies to embed them, or the routes below.

Endpoints

GET/v1/social/commentsA page, with every filter, sort, search, field and expand option below.
POST/v1/social/commentsCreate one. Answers 201 with the record, its id and a Location header — simulated, nothing stored; 422 names each field that is wrong.
GET/v1/social/comments/{id}One record, with an ETag; If-None-Match earns a 304.
PATCH/v1/social/comments/{id}Change the fields you send.
PUT/v1/social/comments/{id}Replace the record; required fields must all be there.
DELETE/v1/social/comments/{id}Answers 200 with what would have been removed and the note (a real server’s 204 lives at /v1/utils/status/204).
GET/v1/social/comments/{id}/postThe post this record points at.
GET/v1/social/comments/{id}/authorThe user this record points at.
GET/v1/social/comments/{id}/parentThe comment this record points at.
GET/v1/social/comments/{id}/repliesIts comments, as a page with all the list options.

Try it

List with a filter
curl "https://api.sondahub.com/v1/social/comments?expand=post&limit=3"
One record
curl https://api.sondahub.com/v1/social/comments/1?expand=post
Its replies
curl "https://api.sondahub.com/v1/social/comments/1/replies?limit=5"
Create (simulated)
curl -X POST https://api.sondahub.com/v1/social/comments \
  -H "Content-Type: application/json" \
  -d '{"post_id":1,"author_id":1,"body":"A body"}'
Change one field (simulated)
curl -X PATCH https://api.sondahub.com/v1/social/comments/1 \
  -H "Content-Type: application/json" \
  -d '{"body":"Changed body"}'
Delete (simulated)
curl -X DELETE https://api.sondahub.com/v1/social/comments/1

likes

A user liking a post. POST one to like; DELETE it to unlike. 8,000 records — the file.

POST refuses a second like of the same post by the same user (409 already_liked) and bumps the post’s likes_count; DELETE lowers it.
FieldTypeNotes
idread-onlyintAssigned by the server. Seed records keep their ids across restarts; records you create continue after the seed.
created_atread-onlydatetimeWhen the record was created (ISO 8601, UTC).
updated_atread-onlydatetimeWhen the record last changed.
user_idrequiredint → users
post_idrequiredint → posts

Relations: user → one user through user_id; post → one post through post_id. Use ?expand=user,post to embed them, or the routes below.

Endpoints

GET/v1/social/likesA page, with every filter, sort, search, field and expand option below.
POST/v1/social/likesCreate one. Answers 201 with the record, its id and a Location header — simulated, nothing stored; 422 names each field that is wrong.
GET/v1/social/likes/{id}One record, with an ETag; If-None-Match earns a 304.
PATCH/v1/social/likes/{id}Change the fields you send.
PUT/v1/social/likes/{id}Replace the record; required fields must all be there.
DELETE/v1/social/likes/{id}Answers 200 with what would have been removed and the note (a real server’s 204 lives at /v1/utils/status/204).
GET/v1/social/likes/{id}/userThe user this record points at.
GET/v1/social/likes/{id}/postThe post this record points at.

Try it

List with a filter
curl "https://api.sondahub.com/v1/social/likes?expand=user&limit=3"
One record
curl https://api.sondahub.com/v1/social/likes/1?expand=user
Create (simulated)
curl -X POST https://api.sondahub.com/v1/social/likes \
  -H "Content-Type: application/json" \
  -d '{"user_id":1,"post_id":1}'
Change one field (simulated)
curl -X PATCH https://api.sondahub.com/v1/social/likes/1 \
  -H "Content-Type: application/json" \
  -d '{}'
Delete (simulated)
curl -X DELETE https://api.sondahub.com/v1/social/likes/1

follows

follower_id follows followee_id. 5,000 records — the file.

POST refuses following yourself and duplicates (409); following a private user starts pending; active follows move both users’ counters.
FieldTypeNotes
idread-onlyintAssigned by the server. Seed records keep their ids across restarts; records you create continue after the seed.
created_atread-onlydatetimeWhen the record was created (ISO 8601, UTC).
updated_atread-onlydatetimeWhen the record last changed.
follower_idrequiredint → users
followee_idrequiredint → users
statusenumactive pending blocked
notificationsbool

Relations: follower → one user through follower_id; followee → one user through followee_id. Use ?expand=follower,followee to embed them, or the routes below.

Endpoints

GET/v1/social/followsA page, with every filter, sort, search, field and expand option below.
POST/v1/social/followsCreate one. Answers 201 with the record, its id and a Location header — simulated, nothing stored; 422 names each field that is wrong.
GET/v1/social/follows/{id}One record, with an ETag; If-None-Match earns a 304.
PATCH/v1/social/follows/{id}Change the fields you send.
PUT/v1/social/follows/{id}Replace the record; required fields must all be there.
DELETE/v1/social/follows/{id}Answers 200 with what would have been removed and the note (a real server’s 204 lives at /v1/utils/status/204).
GET/v1/social/follows/{id}/followerThe user this record points at.
GET/v1/social/follows/{id}/followeeThe user this record points at.

Try it

List with a filter
curl "https://api.sondahub.com/v1/social/follows?status=pending&expand=follower&limit=3"
One record
curl https://api.sondahub.com/v1/social/follows/1?expand=follower
Create (simulated)
curl -X POST https://api.sondahub.com/v1/social/follows \
  -H "Content-Type: application/json" \
  -d '{"follower_id":1,"followee_id":1,"status":"active"}'
Change one field (simulated)
curl -X PATCH https://api.sondahub.com/v1/social/follows/1 \
  -H "Content-Type: application/json" \
  -d '{"status":"pending"}'
Delete (simulated)
curl -X DELETE https://api.sondahub.com/v1/social/follows/1

WebSocket and SSE

The same stream two ways: the world's own activity, one tick a second, generated for your connection alone. Both push JSON text messages; SSE names each one with event: and numbers it with id:. ?topics=a,b narrows either.

TopicWhat arrivesHow often
postsA new post from one of the seed users.5 s
likesSomeone liking something.2 s
WebSocket
wss://api.sondahub.com/v1/social/ws?topics=posts

> {"type":"hello","api":"social","topics":[…],"subscribed":[…]}
> {"type":"event","topic":"posts","api":"social","ts":"…","data":{…}}
< {"type":"subscribe","topics":["posts"]}   # narrow to some topics
< {"type":"ping"}                            # → {"type":"pong"}
< anything else                             # → echoed back as {"type":"echo"}
Server-Sent Events
curl -N "https://api.sondahub.com/v1/social/events?topics=posts"

retry: 3000
id: 1
event: posts
data: {"type":"event","topic":"posts",…}

GraphQL

One endpoint, https://api.sondahub.com/v1/social/graphql: POST {"query", "variables"} or GET ?query=. Introspection is on, so Sonda's GraphQL mode loads the schema; the SDL is a click away. Every collection is a paged query with the same filter, sort and q options as REST (operators as suffixes: price_lt), a by-id query, relation fields both ways, and create, update, replace and delete mutations — simulated like every write, with the note in extensions.

A query
curl https://api.sondahub.com/v1/social/graphql -H "Content-Type: application/json" -d '{"query": "{ posts(limit: 3, sort: \"-id\", filter: { visibility: public }) { total data { id author_id body visibility author { username } comments(limit: 2) { id } } } }"}'
{
  posts(limit: 3, sort: "-id", filter: { visibility: public }) {
    total
    data {
      id author_id body visibility
      author { username }
      comments(limit: 2) { id }
    }
  }
}