Quick Start
Base URL: https://api.codewithbotina.com
Auth header: Authorization: Bearer <access_token>
Auth cookies: cwb_access (access token), cwb_refresh (refresh token)
Admin endpoints: require an authenticated user with is_admin=true.
CORS: production responses include CORS headers for the configured frontend origin (defaults to https://blog.codewithbotina.com).
Responses: many endpoints return a wrapped JSON shape { success, message, data }. Some endpoints return custom JSON on success (explicitly marked below).
Authentication
OAuth Flow (as implemented)
1) Browser navigates to: GET /api/auth/google?next={frontendUrl}
2) Backend sets PKCE cookie and 302 redirects to Supabase OAuth authorize URL
3) Supabase redirects to: {frontendUrl}/auth/callback?code=...&pkce=...&next=...
4) Frontend forwards the code to: GET /api/auth/callback?code=...&pkce=...&next=...
5) Backend exchanges code, sets auth cookies, and 302 redirects to next (or /{lang}/)GET https://api.codewithbotina.com/api/auth/google
Initiate Google OAuth with PKCE and redirect the user.
Auth: Public
Query: next (optional; must be same origin as the configured frontend or it is ignored)
Example
GET /api/auth/google?next=https://blog.codewithbotina.com/es/Success
302 redirect to Supabase OAuth authorize URL. Sets a short-lived cwb_pkce cookie.
Errors
- 429 Too many requests
- 500 Internal server error
GET https://api.codewithbotina.com/api/auth/callback
Finalize OAuth: exchange code for a session, set auth cookies, clear PKCE cookie, then redirect to the frontend.
Auth: Public
Query: code (required), pkce (optional token), next (optional)
Example
GET /api/auth/callback?code=AUTH_CODE&pkce=PKCE_TOKEN&next=https://blog.codewithbotina.com/es/Success
302 redirect to next (if valid) or to /{lang}/ on the configured frontend origin (defaults to /en/).
Errors
- 400 Missing authorization code
- 400 Missing PKCE code verifier
- 500 Internal server error
GET https://api.codewithbotina.com/api/auth/me
Get the current user. If the access token is invalid/expired but a refresh cookie exists, this endpoint attempts a refresh and sets updated cookies.
Auth: Authenticated user
Auth
Authorization: Bearer <access_token>
or Cookie: cwb_access=...; cwb_refresh=...Success (custom JSON)
{
"success": true,
"user": {
"id": "uuid",
"email": "user@example.com",
"full_name": "Name",
"avatar_url": "https://...",
"google_id": "123456",
"created_at": "2026-02-01T10:00:00Z",
"last_login": "2026-02-07T15:30:00Z",
"is_admin": false
}
}Errors
- 401 Unauthorized
- 500 Internal server error
POST https://api.codewithbotina.com/api/auth/refresh
Refresh an access token. Refresh token is taken from the JSON body (refresh_token) or the cwb_refresh cookie.
Auth: Public (refresh token required)
Request body (optional)
{ "refresh_token": "v1.MR..." }Success (custom JSON)
{
"success": true,
"access_token": "eyJhbG...",
"refresh_token": "v1.MR...",
"expires_in": 3600
}Errors
- 401 Missing refresh token
- 500 Internal server error
POST https://api.codewithbotina.com/api/auth/signout
Sign out (Supabase global sign-out) and clear auth cookies.
Auth: Authenticated user
Auth
Authorization: Bearer <access_token>
or Cookie: cwb_access=...Success (custom JSON)
{ "success": true, "message": "Signed out successfully" }Errors
- 401 Unauthorized
- 500 Internal server error
GET https://api.codewithbotina.com/api/auth/debug
Debug endpoint: reports whether auth header/cookies exist and resolves the user if a token is present.
Auth: Public
Success (custom JSON)
{
"hasAuthHeader": true,
"hasCookies": true,
"hasAccessCookie": true,
"hasPkceCookie": false,
"user": { "id": "...", "email": "...", "is_admin": false },
"error": null
}Posts
GET https://api.codewithbotina.com/api/posts
List posts. Supports language filtering and a basic autocomplete query on titulo/slug.
Auth: Public
Query: language (optional), q (optional), limit, offset
Example
GET /api/posts?language=es&q=react&limit=20&offset=0Success (wrapped)
{
"success": true,
"message": "Posts fetched successfully",
"data": {
"posts": [
{ "id": "uuid", "titulo": "...", "slug": "...", "body": "...", "imagen_url": null, "fecha": "...", "updated_at": "...", "language": "es" }
],
"limit": 20,
"offset": 0
}
}GET https://api.codewithbotina.com/api/posts/{slug}
Fetch a post by slug, optionally by language, and include its tags.
Auth: Public
Query: language (optional)
Example
GET /api/posts/my-post?language=enSuccess (wrapped)
{
"success": true,
"message": "Post fetched successfully",
"data": {
"id": "uuid",
"titulo": "...",
"slug": "my-post",
"body": "...",
"imagen_url": null,
"fecha": "...",
"updated_at": "...",
"language": "en",
"tags": [{ "id": "uuid", "name": "Tag", "slug": "tag", "usage_count": 3 }]
}
}GET https://api.codewithbotina.com/api/posts/search
Advanced search with optional date/language/tag filters.
Auth: Public
Query: q, from, to, language or languages, tag_ids or tags or tag_slug, scope, sort, relevance, limit, offset
Notes
- Default
scope=titleuses sequential fallback: title → content → tags. - Response includes
phase: title|content|tags|none. - Tag filters apply AND logic (post must contain all selected tags).
languages=alldisables language filtering.
Success (wrapped)
{
"success": true,
"message": "Posts fetched successfully",
"data": {
"posts": [{ "id": "uuid", "titulo": "...", "slug": "...", "body": "...", "imagen_url": null, "fecha": "...", "language": "en" }],
"total": 123,
"limit": 10,
"offset": 0,
"phase": "title",
"relevance": "most_recent",
"sort": "newest"
}
}POST https://api.codewithbotina.com/api/posts/create
Create posts (admin only). Accepts single post or a batch payload.
Auth: Admin
Request body (single)
{
"titulo": "Title",
"slug": "title",
"body": "Min 100 chars...",
"language": "en",
"imagen_url": "https://...",
"tag_ids": ["uuid"]
}Request body (batch)
{
"posts": [
{ "titulo": "Hola", "slug": "hola", "body": "Min 100 chars...", "language": "es", "tag_ids": [] },
{ "titulo": "Hello", "slug": "hello", "body": "Min 100 chars...", "language": "en", "tag_ids": [] }
]
}Success (wrapped, single)
{
"success": true,
"message": "Post created successfully",
"data": { "id": "uuid", "titulo": "Title", "slug": "title", "fecha": "..." }
}Success (wrapped, batch)
{
"success": true,
"message": "Posts created successfully",
"data": {
"posts": [{ "id": "uuid", "titulo": "Hola", "slug": "hola", "fecha": "...", "language": "es" }],
"translation_group_id": "uuid-or-null"
}
}PUT https://api.codewithbotina.com/api/posts/{slug}/update
Update a post by slug (admin only).
Auth: Admin
Request body
{
"titulo": "Updated",
"slug": "updated",
"body": "Min 100 chars...",
"language": "en",
"imagen_url": null,
"tag_ids": ["uuid"]
}Success (wrapped)
{
"success": true,
"message": "Post updated successfully",
"data": { "id": "uuid", "titulo": "Updated", "slug": "updated" }
}PUT https://api.codewithbotina.com/api/posts/bulk-update
Bulk update/create/unlink translations (admin only).
Auth: Admin
Request body
{
"updates": [{ "post_id": "uuid", "post": { "titulo": "...", "slug": "...", "body": "...", "language": "en" } }],
"creates": [{ "base_post_id": "uuid", "post": { "titulo": "...", "slug": "...", "body": "...", "language": "es" } }],
"unlinks": [{ "post_id": "uuid", "linked_post_id": "uuid" }]
}Success (wrapped)
{
"success": true,
"message": "Posts updated successfully",
"data": {
"updated_post_ids": ["uuid"],
"created_post_ids": ["uuid"],
"unlinked_post_ids": ["uuid"],
"translation_group_id_by_base_post_id": { "uuid": "uuid-or-null" }
}
}DELETE https://api.codewithbotina.com/api/posts/{slug}/delete
Delete a post (admin only). Requires confirm=true to execute.
Auth: Admin
Query: confirm (optional), language (optional)
Success (wrapped, confirm missing/false)
{
"success": true,
"message": "Confirmation required",
"data": { "post_id": "uuid", "titulo": "...", "comments_count": 12, "reactions_count": 30, "likes_count": 25, "dislikes_count": 5, "imagen_url": null, "requires_confirmation": true }
}Success (wrapped, confirm=true)
{
"success": true,
"message": "Post deleted successfully",
"data": { "post_id": "uuid", "comments_deleted": 12, "reactions_deleted": 30 }
}GET https://api.codewithbotina.com/api/posts/{slug}/exists
Check whether a slug exists.
Auth: Public
Query: language (optional)
Success (custom JSON)
{ "exists": true }POST https://api.codewithbotina.com/api/posts/upload-image
Upload a featured image (admin only). Uses multipart/form-data.
Auth: Admin
Form fields
image: file
title: string
slug: stringSuccess (wrapped)
{
"success": true,
"message": "Image uploaded successfully",
"data": { "url": "https://...", "filename": "file.webp", "size": 123 }
}GET https://api.codewithbotina.com/api/posts/{slug}/tags
Fetch tags for a post slug.
Auth: Public
Success (wrapped)
{
"success": true,
"data": { "tags": [{ "id": "uuid", "name": "Tag", "slug": "tag", "description": null, "usage_count": 3 }] }
}GET https://api.codewithbotina.com/api/posts/{postId}/translations
List translations for a post by UUID. Note: the route param name is slug in code, but this endpoint expects a UUID.
Auth: Public
Success (wrapped)
{
"success": true,
"message": "Translations fetched successfully",
"data": {
"translations": [
{ "post_id": "uuid", "language": "es", "slug": "hola", "titulo": "Hola", "fecha": "...", "imagen_url": null, "translation_group_id": "uuid" }
]
}
}POST https://api.codewithbotina.com/api/posts/{postId}/translations
Link/unlink translations (admin only). Empty linked_post_ids unlinks the post from its group.
Auth: Admin
Request body
{ "linked_post_ids": ["uuid", "uuid"] }Success (wrapped)
{
"success": true,
"message": "Translations linked successfully",
"data": { "translation_group_id": "uuid-or-null", "translations": [] }
}GET https://api.codewithbotina.com/api/posts/{postId}/translation/{language}
Fetch the translation summary for a given target language.
Auth: Public
Success (wrapped)
{
"success": true,
"message": "Translation fetched successfully",
"data": { "post_id": "uuid", "language": "en", "slug": "hello", "titulo": "Hello", "fecha": "...", "imagen_url": null, "translation_group_id": "uuid" }
}DELETE https://api.codewithbotina.com/api/posts/{postId}/translations/{linkedPostId}
Unlink a translation (admin only).
Auth: Admin
Success (wrapped)
{
"success": true,
"message": "Translation unlinked successfully",
"data": { "translation_group_id": "uuid-or-null" }
}GET https://api.codewithbotina.com/api/posts/test
Diagnostics endpoint for Supabase connectivity and auth decoding.
Auth: Public (Authorization header is optional)
Success (custom JSON)
{ "success": true, "logs": ["..."], "timestamp": "..." }Reactions
GET https://api.codewithbotina.com/api/reactions/{postId}
Get like/dislike counts for a post.
Auth: Public
Success (custom JSON)
{ "success": true, "data": { "post_id": "uuid", "likes": 10, "dislikes": 2, "total": 12 } }POST https://api.codewithbotina.com/api/reactions/{postId}/like
Toggle like (authenticated user).
Auth: Authenticated user
Success (custom JSON)
{ "success": true, "data": { "reaction": "like", "counts": { "likes": 11, "dislikes": 2, "total": 13 } } }POST https://api.codewithbotina.com/api/reactions/{postId}/dislike
Toggle dislike (authenticated user).
Auth: Authenticated user
Success (custom JSON)
{ "success": true, "data": { "reaction": "dislike", "counts": { "likes": 10, "dislikes": 3, "total": 13 } } }GET https://api.codewithbotina.com/api/reactions/user/{postId}
Get the current user reaction for a post.
Auth: Authenticated user
Success (custom JSON)
{ "success": true, "data": { "user_reaction": "like" } }Storage
GET https://api.codewithbotina.com/api/storage/images
List images in the storage bucket (admin only).
Auth: Admin
Query: limit, offset, q (optional)
Success (wrapped)
{
"success": true,
"message": "Images fetched successfully",
"data": { "bucket": "blog-images", "images": [], "limit": 48, "offset": 0, "next_offset": 48, "has_more": false }
}Users
GET https://api.codewithbotina.com/api/users/profile
Fetch the current user profile stats and liked posts.
Auth: Authenticated user
Query: limit (1..200), offset
Success (wrapped)
{
"success": true,
"message": "Profile fetched successfully",
"data": { "stats": { "likes_given": 0, "dislikes_given": 0, "comments_posted": 0 }, "liked_posts_total": 0, "liked_posts": [], "pagination": { "limit": 20, "offset": 0 } }
}POST https://api.codewithbotina.com/api/users/delete-account
Delete the current user account (authenticated user) and clear auth cookies.
Auth: Authenticated user
Success (wrapped)
{ "success": true, "message": "Account deleted successfully", "data": { "deleted": true } }Contact
POST https://api.codewithbotina.com/api/contact
Submit a contact form message.
Auth: Public
Request body
{
"nombre": "string (max 100 chars)",
"correo": "valid email",
"mensaje": "string (10-1000 chars)"
}Success (wrapped)
{
"success": true,
"message": "Contact form submitted successfully",
"data": { "id": "uuid", "nombre": "Name", "correo": "email@example.com", "mensaje": "...", "created_at": "..." }
}Errors
- 400 Validation failed (details provided)
- 429 Too many requests
- 500 Internal server error
⚡ Try It Out
Test the live API directly from your browser.
Health
GET https://api.codewithbotina.com/api/health
Service health check for monitoring.
Auth: Public
Success (custom JSON)
{ "status": "ok", "timestamp": "2026-03-01T12:00:00Z" }Error Handling
Most endpoints return errors using the backend helper errorResponse:
{
"success": false,
"error": "Error message",
"details": { "field": "optional detail" }
}Endpoints marked “custom JSON” still use errorResponse for errors.
Comments
Route note:
/api/comments/{commentId}is used for all methods. In the implementation,GETandPOSTtreat that parameter as a post id, whilePUTandDELETEtreat it as a comment id.GET
https://api.codewithbotina.com/api/comments/{postId}List comments for a post.
Auth: Public
Success (custom JSON)
POST
https://api.codewithbotina.com/api/comments/{postId}Create a comment for a post.
Auth: Authenticated user
Request body
Success (custom JSON)
PUT
https://api.codewithbotina.com/api/comments/{commentId}Update a comment (author only).
Auth: Authenticated user
Request body
Success (custom JSON)
DELETE
https://api.codewithbotina.com/api/comments/{commentId}Delete a comment (author or admin).
Auth: Authenticated user
Success (custom JSON)
POST
https://api.codewithbotina.com/api/comments/{commentId}/pinPin a comment (admin only).
Auth: Admin
Success (custom JSON)
POST
https://api.codewithbotina.com/api/comments/{commentId}/unpinUnpin a comment (admin only).
Auth: Admin
Success (custom JSON)