API Documentation
Everything Simple Social does goes through this API, so you can build your own front end with it. Please follow the Code of Conduct when using it.
Basics
Endpoints
Requests
Every request is an HTTP POST. Send parameters as application/x-www-form-urlencoded, except uploadMedia, which uses multipart/form-data. JSON request bodies are not supported. The action parameter picks what to do.
POST /api.php
Content-Type: application/x-www-form-urlencoded
action=login&email=you%40example.com&password=YourPassword1!
Parameters that take a list of IDs (userIds, postIds) expect a JSON array encoded as a string, for example postIds=["26.1789611603","8.1"].
Authentication
Call login to get a JWT, then send it on every protected request:
Authorization: Bearer <jwt>
- Tokens expire 24 hours after login.
- Only your most recent login's token is valid. Logging in again, from any app, invalidates the previous token.
logout invalidates the token immediately.
Actions marked Auth: none work without a token. Everything else returns 401 without a valid one.
Attempt limits
login, verifyOTP, and verifyRegisterOTP count failed attempts. After 5 failures for the same email, or 20 from the same IP address, within 15 minutes, further attempts return 429 for 15 minutes. Locking an OTP check also cancels the current code, so a new one has to be requested.
Responses
Responses are JSON. Successful responses have "valid": true plus the fields documented for each action:
{ "valid": true, "postId": "26.1789611603" }
Errors have "valid": false and a human-readable message. Most errors use the key message, but some general errors (see below) use error, so read whichever is present:
{ "valid": false, "message": "Post not found" }
{ "valid": false, "error": "Unauthorized" }
Errors any action can return
Data formats
- Timestamps are strings in server time:
"2026-09-16 19:20:03". Follow timestamps from very old accounts may be the string "Unknown".
- Post IDs are strings in the form
"<ownerUserId>.<unixTime>", for example "26.1789611603".
- User IDs are numbers.
- Media URLs are paths like
/media/26/image/26_image_20260916192002_591227.webp. Prefix them with https://dev.davidfruin.com to load the file. Posts without media have "mediaUrl": null.
- Text limits: posts and comments are limited to 5000 characters and may only contain printable ASCII, newlines, and Latin-1 characters (such as é, ñ, ¿). Emoji are rejected.
Post object
Returned by the post-listing actions and getPostById:
{
"id": "26.1789611603",
"text": "Testing the new interactive CLI wizard",
"timestamp": "2026-09-16 19:20:03",
"likes": [
{ "userId": 1, "timestamp": "2026-09-16 19:43:02" }
],
"mediaUrl": "/media/26/image/26_image_20260916192002_591227.webp",
"userID": 26,
"userEmail": "davefruin@gmail.com"
}
Actions
Account
login
POST /api.php · Auth: none
Signs in and returns a JWT for protected actions.
| Parameter | Required | Description |
email | yes | Account email (not case-sensitive) |
password | yes | Account password |
{
"valid": true,
"message": "Login successful",
"userId": 26,
"jwt": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}
| Status | message |
| 400 | Missing credentials |
| 401 | Invalid email or password |
| 429 | Too many attempts. Try again in N minutes. |
logout
POST /api.php · Auth: none (send the token to invalidate it)
Invalidates the token sent in the Authorization header. Always succeeds.
{ "valid": true, "message": "Logged out" }
sendRegisterOTP
POST /api.php · Auth: none
Step 1 of registration. Emails a 6-digit code that is valid for 10 minutes. Requesting again replaces the previous code.
| Parameter | Required | Description |
email | yes | Email to register |
{ "valid": true, "message": "OTP sent to your email. Check inbox/spam." }
| Status | message |
| 400 | Valid email required |
| 400 | Email already registered |
| 500 | Failed to send email. |
verifyRegisterOTP
POST /api.php · Auth: none
Step 2 of registration. Checks the emailed code. After it succeeds, you have 10 minutes to call finishRegister.
| Parameter | Required | Description |
email | yes | Email being registered |
otp | yes | 6-digit code from the email |
{ "valid": true, "message": "OTP verified! Set your password." }
| Status | message |
| 400 | Email and 6-digit OTP required |
| 400 | No OTP requested |
| 400 | OTP has expired. Please request a new one. |
| 400 | Incorrect OTP |
| 429 | Too many attempts. Try again in N minutes. |
finishRegister
POST /api.php · Auth: none
Step 3 of registration. Call within 10 minutes after verifyRegisterOTP succeeds. Afterwards, call login.
| Parameter | Required | Description |
email | yes | Email being registered |
password | yes | 8–25 characters with a lowercase letter, an uppercase letter, a number, and a symbol |
confirm | yes | Must match password |
{ "valid": true, "message": "Account created successfully!" }
| Status | message |
| 400 | Passwords do not match or are empty |
| 400 | Password must be 8-25 characters |
| 400 | Password must contain a lowercase letter |
| 400 | Password must contain an uppercase letter |
| 400 | Password must contain a number |
| 400 | Password must contain a symbol |
| 400 | Session expired. Please start over. |
| 403 | Verify your OTP before creating your account |
sendOTP
POST /api.php · Auth: none
Step 1 of a password reset. Emails a 6-digit code that is valid for 10 minutes.
| Parameter | Required | Description |
email | yes | Account email |
{ "valid": true, "message": "OTP sent to your email. Check inbox/spam." }
| Status | message |
| 400 | Valid email required |
| 404 | No account found with this email |
| 500 | Failed to send email. Try again or contact support. |
verifyOTP
POST /api.php · Auth: none
Step 2 of a password reset. Checks the emailed code. After it succeeds, you have 10 minutes to call resetPassword.
| Parameter | Required | Description |
email | yes | Account email |
otp | yes | 6-digit code from the email |
{ "valid": true, "message": "OTP verified! Set your new password." }
| Status | message |
| 400 | No OTP requested or expired |
| 400 | OTP has expired. Please request a new one. |
| 400 | Incorrect OTP |
| 429 | Too many attempts. Try again in N minutes. |
resetPassword
POST /api.php · Auth: none
Step 3 of a password reset. Call within 10 minutes after verifyOTP succeeds. Each verification allows one reset.
| Parameter | Required | Description |
email | yes | Account email |
password | yes | New password: 8–25 characters with a lowercase letter, an uppercase letter, a number, and a symbol |
confirm | yes | Must match password |
{ "valid": true, "message": "Password reset successful! Please log in." }
| Status | message |
| 400 | Passwords do not match or are empty |
| 400 | Password must be 8-25 characters |
| 400 | Password must contain a lowercase letter |
| 400 | Password must contain an uppercase letter |
| 400 | Password must contain a number |
| 400 | Password must contain a symbol |
| 403 | Verify your OTP before resetting your password |
| 404 | User not found |
deleteAccount
POST /api.php · Auth: required
Permanently deletes your account, posts, comments, likes, follows, notifications, and uploaded media. This cannot be undone.
| Parameter | Required | Description |
password | yes | Your current password |
{ "valid": true, "message": "Account deleted successfully" }
| Status | message |
| 400 | Password required |
| 401 | Incorrect password |
Users
getMyInfo
POST /api.php · Auth: required
Returns the signed-in user. id and userId are the same value.
{
"valid": true,
"id": 26,
"userId": 26,
"email": "davefruin@gmail.com",
"created_at": "2026-04-13 22:17:00"
}
getUserInfo
POST /api.php · Auth: required
Returns another user's email and join date. created_at is "Unknown" if it wasn't recorded.
| Parameter | Required | Description |
userId | yes | User ID |
{ "valid": true, "email": "annabegomez@gmail.com", "created_at": "2026-02-25 22:54:00" }
| Status | message |
| 400 | Invalid user ID |
| 404 | User not found |
getUsers
POST /api.php · Auth: required
Returns every user except you, sorted by email.
{
"valid": true,
"users": [
{ "id": 8, "email": "annabegomez@gmail.com", "created_at": "2026-02-25 22:54:00" }
]
}
getUserEmails
POST /api.php · Auth: required
Looks up emails for a list of user IDs. The result is keyed by user ID. IDs that don't exist are left out.
| Parameter | Required | Description |
userIds | yes | JSON array of user IDs, e.g. [1,8] |
{ "valid": true, "emails": { "1": "me@davidfruin.com", "8": "annabegomez@gmail.com" } }
Posts
post
POST /api.php · Auth: required
Creates a post. To attach media, upload it first with uploadMedia and pass the returned mediaUrl.
| Parameter | Required | Description |
postText | yes | Post text (up to 5000 characters, see text limits) |
mediaUrl | no | A mediaUrl you uploaded |
{ "valid": true, "postId": "26.1789611603" }
| Status | message |
| 400 | Post text required |
| 400 | You are trying to make a post that is longer than 5K characters |
| 400 | You are trying to post illegal characters |
| 400 | Invalid mediaUrl or not owned by user |
deletePost
POST /api.php · Auth: required
Deletes one of your posts, along with its media and related notifications.
| Parameter | Required | Description |
postId | yes | ID of your post |
{ "valid": true, "deleted": true }
| Status | message |
| 400 | Missing post ID |
| 403 | You can only delete your own posts |
getPostById
POST /api.php · Auth: required
Returns a single post object.
| Parameter | Required | Description |
postId | yes | Post ID |
{ "valid": true, "post": { "id": "26.1789611603", "text": "...", ... } }
| Status | message |
| 400 | Post ID required |
| 404 | Post not found |
getMyPosts
POST /api.php · Auth: required
Your posts, newest first. totalCount is the total across all pages. hasMore tells you whether to request the next page.
| Parameter | Required | Description |
offset | no | Posts to skip (default 0) |
limit | no | Posts to return (default 25) |
{ "valid": true, "posts": [ ... ], "hasMore": true, "totalCount": 42 }
getUserPosts
POST /api.php · Auth: required
A user's posts, newest first. The response format is the same as getMyPosts.
| Parameter | Required | Description |
userId | yes | User ID |
offset | no | Posts to skip (default 0) |
limit | no | Posts to return (default 25) |
{ "valid": true, "posts": [ ... ], "hasMore": false, "totalCount": 1 }
| Status | message |
| 400 | Invalid user ID |
fetchFollowedPosts
POST /api.php · Auth: required
Your news feed: posts from everyone you follow plus your own, newest first. The response format is the same as getMyPosts.
| Parameter | Required | Description |
offset | no | Posts to skip (default 0) |
limit | no | Posts to return (default 25) |
{ "valid": true, "posts": [ ... ], "hasMore": true, "totalCount": 130 }
getPostPreviews
POST /api.php · Auth: required
Short previews for a list of posts: the first 25 characters of each post's text followed by .... Posts that don't exist or have no text are left out.
| Parameter | Required | Description |
postIds | yes | JSON array of post IDs |
{ "valid": true, "previews": { "26.1789611603": "Testing the new interactiv..." } }
Likes
likePost
POST /api.php · Auth: required
Likes a post and notifies its owner. Liking a post you already liked does nothing.
| Parameter | Required | Description |
postId | yes | Post ID |
{ "valid": true, "liked": true }
| Status | message |
| 400 | Missing post ID |
| 400 | Cannot like your own post |
| 404 | Post owner not found |
| 404 | Post not found |
unlikePost
POST /api.php · Auth: required
Removes your like from a post.
| Parameter | Required | Description |
postId | yes | Post ID |
{ "valid": true, "liked": false }
| Status | message |
| 400 | Missing post ID |
| 404 | Post owner not found |
| 404 | Post not found |
getPostLikes
POST /api.php · Auth: required
Who liked a post. Use getUserEmails to look up their emails.
| Parameter | Required | Description |
postId | yes | Post ID |
{ "valid": true, "likes": [ { "userId": 1, "timestamp": "2026-09-16 19:43:02" } ] }
| Status | message |
| 400 | Missing post ID |
| 404 | Post not found |
Follows
followUser
POST /api.php · Auth: required
Follows a user and notifies them. Following someone you already follow does nothing.
| Parameter | Required | Description |
userId | yes | User to follow (not yourself) |
{ "valid": true, "following": true }
| Status | message |
| 400 | Invalid user ID |
unfollowUser
POST /api.php · Auth: required
Unfollows a user and notifies them.
| Parameter | Required | Description |
userId | yes | User to unfollow (not yourself) |
{ "valid": true, "following": false }
| Status | message |
| 400 | Invalid user ID |
isFollowing
POST /api.php · Auth: required
Whether you follow a user.
| Parameter | Required | Description |
userId | yes | User ID (not yourself) |
{ "valid": true, "following": true }
| Status | message |
| 400 | Invalid user ID |
getMyFollows
POST /api.php · Auth: required
Who a user follows, defaulting to you. timestamp is when they followed, or "Unknown" for very old follows.
| Parameter | Required | Description |
userId | no | User ID (default: you) |
{
"valid": true,
"follows": [
{ "id": 8, "email": "annabegomez@gmail.com", "timestamp": "2026-04-13 22:54:31" }
]
}
| Status | message |
| 400 | Invalid user ID |
getMyFollowers
POST /api.php · Auth: required
Who follows a user, defaulting to you. Entries have the same format as getMyFollows.
| Parameter | Required | Description |
userId | no | User ID (default: you) |
{
"valid": true,
"followers": [
{ "id": 1, "email": "me@davidfruin.com", "timestamp": "2026-04-27 14:41:50" }
]
}
| Status | message |
| 400 | Invalid user ID |
Notifications
getNotifications
POST /api.php · Auth: required
Your notifications, newest first, 25 at a time. Request the next page with offset until fewer than 25 come back. type is one of follow, unfollow, like, unlike, or comment. post_id is null for follow notifications.
| Parameter | Required | Description |
offset | no | Notifications to skip (default 0) |
{
"valid": true,
"notifications": [
{
"id": 120,
"recipient_id": 26,
"actor_id": 1,
"actor_email": "me@davidfruin.com",
"type": "like",
"post_id": "26.1789611603",
"created_at": "2026-09-16 19:43:02"
}
]
}
getUnseenNotificationCount
POST /api.php · Auth: required
How many notifications arrived since you last called markNotificationsSeen.
{ "valid": true, "count": 3 }
markNotificationsSeen
POST /api.php · Auth: required
Resets your unseen notification count to zero.
{ "valid": true, "message": "Notifications marked as seen" }
Internal
log
POST /api.php · Auth: required
Used by the website to report client-side errors. Third-party front ends don't need it.
| Parameter | Required | Description |
level | no | DEBUG, INFO, WARN, or ERROR (default) |
category | no | frontend (default), api, access, or php |
message | no | Log message (line breaks are removed; cut to 2000 characters) |
extra | no | Extra details (same limits as message) |
{ "valid": true, "message": "Logged" }