Postman Collection

Weekend Wanderer API Reference

RESTful JSON API. All endpoints are versioned under /api/v1/.

Base URL: https://weekendwand.laravelsiteshub.com/api/v1

Health

GET /api/health

Liveness probe. Unversioned (note the missing /v1), unauthenticated and unthrottled, and the only endpoint that answers outside the success/data/message envelope — the payload is the top-level object.

Response example
{ "status": "ok", "version": "1.0.0", "php": "8.4.23", "laravel": "13.14.0", "database": "connected", "cache": "working", "timestamp": "2026-07-29T08:49:37+00:00" }

Authentication

Admin endpoints require a Bearer token obtained via POST /api/v1/admin/login. Pass the token in the Authorization header on every protected request.

1. Obtain a token

curl -s -X POST https://weekendwand.laravelsiteshub.com/api/v1/admin/login \ -H "Content-Type: application/json" \ -d '{"email":"admin@example.com","password":"your-admin-password"}'

2. Use the token

curl -s https://weekendwand.laravelsiteshub.com/api/v1/admin/articles \ -H "Authorization: Bearer <token>"

3. Revoke the token

curl -s -X POST https://weekendwand.laravelsiteshub.com/api/v1/admin/logout \ -H "Authorization: Bearer <token>"

Tokens expire after 24 hours. Multiple concurrent sessions are supported: each login mints an additional token and leaves existing ones working, so one account can be signed in from a phone, a laptop and a CI job at once. There is no single-session limit and no token cap.

Action Calling token The account's other tokens
POST /admin/loginn/a — mints a new oneUntouched, all stay valid
POST /admin/logoutRevokedUntouched, all stay valid
PUT /admin/profile changing the passwordSurvivesAll revoked
24 hours elapsingExpiresEach on its own 24-hour clock

Signing out on one device therefore leaves the others signed in. The single exception is a password change through PUT /admin/profile — the “I may be compromised” case — which revokes every other token on the account. Treat a sudden 401 on a previously working token as “the password was changed elsewhere”, not as a bug.

Login answers 401 for a wrong password and for a correct password on a non-admin account — the two are deliberately indistinguishable. A valid token belonging to a non-admin account gets 403 Forbidden from the admin endpoints, not 401.

Response Format

Every response is JSON with a consistent envelope.

Success

{ "success": true, "data": { ... }, "message": "" }

Paginated list

{ "success": true, "data": { "data": [ ... ], "links": { "first": "...", "last": "...", "prev": null, "next": "..." }, "meta": { "current_page": 1, "per_page": 15, "total": 42, "last_page": 3 } }, "message": "" }

Error

{ "success": false, "message": "Validation failed", "errors": { "email": ["The email field is required."] } }

HTTP status codes used

CodeMeaning
200 OK
201 Created
401 Unauthenticated — missing, invalid or expired token; also returned for bad login credentials
403 Forbidden — the token belongs to a non-admin account, or demo mode is blocking a write
404 Not Found
409 Conflict (e.g. duplicate email, or deleting a category that still has articles)
422 Validation error
429 Rate limit exceeded
500 Server error

Rate Limits

Limits are keyed on the caller's IP address, except the authenticated bucket, which is keyed on the admin user id and so is shared across every token that account holds. Exceeding a limit returns HTTP 429. Every throttled response carries X-RateLimit-Limit and X-RateLimit-Remaining. GET /api/health is not throttled at all.

Scope Limit Applies to
Public60 / minute/home, /articles, /categories, /tags, /search, /settings
Authenticated120 / minuteAll /admin/* endpoints except login
Admin login5 / 15 minutesPOST /admin/login
Affiliate30 / minuteGET /affiliates/{slug}/redirect
Newsletter5 / minutePOST /newsletter/subscribe
Contact5 / minutePOST /contact

Home

GET /api/v1/home

Everything the rendered homepage shows, from the same source, so the two cannot drift apart. Sections are cached for 300 seconds; the cache key folds in the resolved category ids and the next scheduled publication, so a renamed category or an article falling due is reflected immediately.

KeyTypeContents
latest_articlesarrayUp to 6 newest published articles from latest_category
featured_articlesarrayUp to 4 newest published featured articles from featured_category
popular_guidesarrayUp to 6 published articles site-wide, by descending view_count
statsarrayThree {"value","label"} pairs: Weekend Guides, Categories, Guide Views. value is a pre-abbreviated string ("1.2K"), not a number
latest_categoryobject|nullThe category the latest_articles cards came from
featured_categoryobject|nullThe category the featured_articles cards came from

The two categories come from configured slugs, but those are only a preference — categories are created and deleted freely in the admin panel. When a slug no longer resolves, the section falls back to whichever real category currently holds the newest matching published article, preferring one the other section is not already using. On an empty site either can still be null, in which case its article array is []. Clients must hide the section and its “view all” link when the category is null rather than linking to a slug that would 404.

Response example
{ "success": true, "data": { "latest_articles": [ ... ], "featured_articles": [ ... ], "popular_guides": [ ... ], "stats": [ { "value": "22", "label": "Weekend Guides" }, { "value": "4", "label": "Categories" }, { "value": "62K", "label": "Guide Views" } ], "latest_category": { "id": 1, "name": "Itineraries", "slug": "itineraries", "description": "Day-by-day travel plans...", "article_count": 11 }, "featured_category": { "id": 3, "name": "Road Trips", "slug": "road-trips", "description": "Scenic drives and open-road adventures...", "article_count": 3 } }, "message": "" }

Articles

GET /api/v1/articles

Returns a paginated list of published articles (15 per page).

Query Parameters

NameTypeRequiredDescription
categorystringNoFilter by category slug
tagstringNoFilter by tag slug
searchstringNoSubstring match on title or plain-text body. Max 255
is_featuredbooleanNo1/true keeps only featured, 0/false keeps only non-featured. Any other value is a 422
sortstringNopopular orders by view count; anything else, including an unrecognised value, falls back to newest first
pageintegerNoPage number (default: 1)
Response example
{ "success": true, "data": { "data": [ { "id": 1, "title": "48 Hours in Paris", "slug": "48-hours-in-paris", "excerpt": "The City of Light never disappoints...", "featured_image": "https://example.com/paris.jpg", "reading_time": 5, "view_count": 1240, "published_at": "2024-06-01T10:00:00+00:00", "category": { "id": 2, "name": "Europe", "slug": "europe" }, "tags": [{ "id": 3, "name": "Budget", "slug": "budget" }] } ], "links": { "first": "...", "last": "...", "prev": null, "next": "..." }, "meta": { "current_page": 1, "per_page": 15, "total": 42, "last_page": 3 } }, "message": "" }
GET /api/v1/articles/{slug}

Returns a single published article by slug; an unpublished, future-dated or unknown slug is a 404. Increments the view counter once per IP, so repeat requests from the same client do not inflate view_count. Includes the sanitized HTML body, a toc array built from its headings, and up to 4 related articles (same category, topped up from same tags, then latest).

Note: The article's canonical web page at /articles/{slug} also emits full Open Graph (og:title, og:description, og:image, og:url, og:type, article:published_time, article:section) and Twitter Card (twitter:card summary_large_image, twitter:title, twitter:description, twitter:image) meta tags for rich social sharing previews.

Response example
{ "success": true, "data": { "id": 1, "title": "48 Hours in Paris", "slug": "48-hours-in-paris", "excerpt": "The City of Light never disappoints...", "featured_image": "https://example.com/paris.jpg", "reading_time": 5, "view_count": 1241, "published_at": "2024-06-01T10:00:00+00:00", "category": { "id": 2, "name": "Europe", "slug": "europe" }, "tags": [], "body": "<p>The City of Light...</p>", "toc": [ { "level": 2, "text": "Day 1: Arrival", "id": "day-1-arrival" } ], "meta_title": "48 Hours in Paris | Weekend Wanderer", "meta_description": "...", "is_published": true, "is_featured": false, "related_articles": [ ... ] }, "message": "" }

Categories

GET /api/v1/categories

Returns all categories ordered by name — not paginated, so there is no links/meta wrapper here. article_count counts only published articles, so it matches exactly what /categories/{slug} will page through. (The admin listing counts drafts and binned articles too, and so can report a higher number for the same category.)

Response example
{ "success": true, "data": [ { "id": 1, "name": "Asia", "slug": "asia", "description": "...", "article_count": 12 } ], "message": "" }
GET /api/v1/categories/{slug}

Returns a category and its paginated published articles (15 per page).

Response example
{ "success": true, "data": { "category": { "id": 2, "name": "Europe", "slug": "europe", "description": "...", "article_count": 8 }, "articles": { "data": [ ... ], "links": { ... }, "meta": { ... } } }, "message": "" }

Tags

GET /api/v1/tags

Returns all tags ordered by name — not paginated. Each carries a published-only article_count. Fields: id, name, slug, article_count — tags have no description.

GET /api/v1/tags/{slug}

Returns { "tag": { ... }, "articles": { "data", "links", "meta" } } — the tag plus its published articles, 15 per page. Mirrors /categories/{slug}.

Affiliates

GET /api/v1/affiliates/{slug}/redirect

Logs a click and returns the partner URL. Despite the path, it does not issue an HTTP redirect — it answers 200 with the usual envelope, and the client navigates to data.redirect_url itself. Unknown slugs return 404. The returned URL is always http or https; other schemes are rejected at write time.

Response example
{ "success": true, "data": { "redirect_url": "https://booking.com/?aid=..." }, "message": "" }

Settings

GET /api/v1/settings

Returns public site settings: site_name, site_description, ga_id, contact_email, site_logo and site_favicon. No other key is exposed.

Unlike the admin endpoint, site_logo and site_favicon here are ready-to-use absolute URLs, not stored paths, and fall back to the bundled assets when nothing has been uploaded — so they are never empty. They are derived from APP_URL rather than the requesting host, so a stale APP_URL in production yields URLs the client cannot reach.

Response example
{ "success": true, "data": { "site_name": "Weekend Wanderer", "site_description": "Curated weekend travel guides", "ga_id": "G-XXXXXXXXXX", "contact_email": "hello@example.com", "site_logo": "https://example.com/assets/logo.png", "site_favicon": "https://example.com/assets/favicon.ico" }, "message": "" }

Newsletter

POST /api/v1/newsletter/subscribe

Subscribes an email address. Returns 201 on first subscription, 409 if already subscribed.

Body (JSON)

FieldTypeRequiredRules
emailstringYesValid email, max 255 chars

Contact

POST /api/v1/contact

Stores a contact message. Returns 201 on success.

Body (JSON)

FieldTypeRequiredRules
namestringYesMax 100 chars
emailstringYesValid email, max 255 chars
messagestringYesMax 5000 chars
Admin Endpoints

All admin endpoints require the Authorization: Bearer <token> header. Obtain a token via Admin Auth → Login. Missing or expired tokens get 401; a valid token on a non-admin account gets 403. When the deployment runs with demo mode enabled, reads still work but every write answers 403 "Demo mode is enabled." — logout is the one exception, so a demo session is never trapped.

Admin — Auth

POST /api/v1/admin/login

Authenticates using email and password. Returns a 24-hour Bearer token. Rate-limited to 5 attempts per 15 minutes.

Body (JSON)

FieldTypeRequiredDescription
emailstringYesAdmin email address
passwordstringYesAdmin password
Response example
{ "success": true, "data": { "token": "1|abc123..." }, "message": "" }
POST /api/v1/admin/logout 🔒 Auth required

Revokes the current token. No body required.

Admin — Articles

GET /api/v1/admin/articles 🔒 Auth required

Paginated list, 15 per page, newest first. ?status=all|published|draft|trashed (default all; an unrecognised value falls back to all rather than erroring). ?search= matches the title only. status=published includes future-scheduled articles, which the public endpoints hide.

POST /api/v1/admin/articles 🔒 Auth required

Create an article. Responds 201. If a soft-deleted article already holds the requested slug it is recycled instead: the binned row is restored in place, keeping its id, resetting view_count to 0 and discarding what it previously contained.

Request body fields
FieldTypeRequiredRules
titlestringYesMax 255
slugstringYesLowercase letters, digits, - and _ only. Max 255. Unique among live articles — a slug held only by a binned article is accepted and recycles that row
contentstringYesHTML. Max 65,535 chars. Sanitized on the way out; excerpt and reading_time are derived from it and cannot be set directly
category_idintegerYesMust exist in categories
tagsarrayNoTag IDs; each must exist. Replaces the existing set outright — omitting it removes every tag, it does not preserve them
is_publishedbooleanNoDefaults to false when omitted
is_featuredbooleanNoDefaults to false when omitted
meta_titlestringNoMax 255
meta_descriptionstringNoMax 500
featured_imagefileNoJPG, JPEG, PNG or WebP. Max 2 MB. Requires multipart/form-data. Omitting it leaves the current image untouched
published_atdatetimeNoAny parseable date; a future date schedules the article. Filled with the current time when is_published is true and no date is set
GET /api/v1/admin/articles/{id} 🔒 Auth required

Get a single article by numeric ID. Non-numeric ids do not match the route and return 404.

PUT /api/v1/admin/articles/{id} 🔒 Auth required

Update an article. Full-representation: title, slug, content and category_id are required on every call, and omitting is_published or is_featured resets them to false. Binned articles cannot be edited — restore first, or this returns 404.

Request body fields
FieldTypeRequiredRules
titlestringYesMax 255
slugstringYesLowercase letters, digits, - and _ only. Max 255. Unique among live articles (ignores own record)
contentstringYesHTML. Max 65,535 chars. Sanitized on the way out; excerpt and reading_time are derived from it and cannot be set directly
category_idintegerYesMust exist in categories
tagsarrayNoTag IDs; each must exist. Replaces the existing set outright — omitting it removes every tag, it does not preserve them
is_publishedbooleanNoDefaults to false when omitted
is_featuredbooleanNoDefaults to false when omitted
meta_titlestringNoMax 255
meta_descriptionstringNoMax 500
featured_imagefileNoJPG, JPEG, PNG or WebP. Max 2 MB. Requires multipart/form-data. Omitting it leaves the current image untouched
published_atdatetimeNoAny parseable date; a future date schedules the article. Filled with the current time when is_published is true and no date is set — an existing date is kept, never overwritten
PATCH /api/v1/admin/articles/{id} 🔒 Auth required

Identical to PUT — same handler, same required fields. This is not a partial update.

Request body fields
FieldTypeRequiredRules
titlestringYesMax 255
slugstringYesLowercase letters, digits, - and _ only. Max 255. Unique among live articles (ignores own record)
contentstringYesHTML. Max 65,535 chars. Sanitized on the way out; excerpt and reading_time are derived from it and cannot be set directly
category_idintegerYesMust exist in categories
tagsarrayNoTag IDs; each must exist. Replaces the existing set outright — omitting it removes every tag, it does not preserve them
is_publishedbooleanNoDefaults to false when omitted
is_featuredbooleanNoDefaults to false when omitted
meta_titlestringNoMax 255
meta_descriptionstringNoMax 500
featured_imagefileNoJPG, JPEG, PNG or WebP. Max 2 MB. Requires multipart/form-data. Omitting it leaves the current image untouched
published_atdatetimeNoAny parseable date; a future date schedules the article. Filled with the current time when is_published is true and no date is set — an existing date is kept, never overwritten
DELETE /api/v1/admin/articles/{id} 🔒 Auth required

Soft-delete an article. It leaves the public endpoints but stays retrievable under ?status=trashed.

POST /api/v1/admin/articles/{id}/restore 🔒 Auth required

Restore a soft-deleted article. Returns the restored article.

Request body fields
FieldTypeRequiredRules
titlestringYesMax 255
slugstringYesLowercase letters, digits, - and _ only. Max 255. Unique among live articles — a slug held only by a binned article is accepted and recycles that row
contentstringYesHTML. Max 65,535 chars. Sanitized on the way out; excerpt and reading_time are derived from it and cannot be set directly
category_idintegerYesMust exist in categories
tagsarrayNoTag IDs; each must exist. Replaces the existing set outright — omitting it removes every tag, it does not preserve them
is_publishedbooleanNoDefaults to false when omitted
is_featuredbooleanNoDefaults to false when omitted
meta_titlestringNoMax 255
meta_descriptionstringNoMax 500
featured_imagefileNoJPG, JPEG, PNG or WebP. Max 2 MB. Requires multipart/form-data. Omitting it leaves the current image untouched
published_atdatetimeNoAny parseable date; a future date schedules the article. Filled with the current time when is_published is true and no date is set
DELETE /api/v1/admin/articles/{id}/force 🔒 Auth required

Permanently delete an article that is already in the bin — it must be soft-deleted first, or this returns 404. Irreversible: the row, its tag links and its uploaded image are all removed.

Admin — Categories

article_count here counts every article in the category — drafts, future-scheduled and binned included — so it will not always agree with the published-only count the public /categories endpoint reports for the same category.

GET /api/v1/admin/categories 🔒 Auth required

Paginated list ordered by name, 20 per page — not the full set in one response.

POST /api/v1/admin/categories 🔒 Auth required

Create a category. Responds 201.

Request body fields
FieldTypeRequiredRules
namestringYesMax 100
slugstringYesLowercase letters, digits, - and _ only. Max 100. Unique
descriptionstringNoMax 1000
PUT /api/v1/admin/categories/{id} 🔒 Auth required

Update a category. Full-representation: name and slug are required on every call.

Request body fields
FieldTypeRequiredRules
namestringYesMax 100
slugstringYesLowercase letters, digits, - and _ only. Max 100. Unique (ignores own record)
descriptionstringNoMax 1000
PATCH /api/v1/admin/categories/{id} 🔒 Auth required

Identical to PUT — same handler, same required fields.

Request body fields
FieldTypeRequiredRules
namestringYesMax 100
slugstringYesLowercase letters, digits, - and _ only. Max 100. Unique (ignores own record)
descriptionstringNoMax 1000
DELETE /api/v1/admin/categories/{id} 🔒 Auth required

Delete a category. Returns 409 if any article still points at it, including soft-deleted ones sitting in the bin.

Admin — Tags

GET /api/v1/admin/tags 🔒 Auth required

Paginated list ordered by name, 20 per page — not the full set in one response. article_count includes drafts and binned articles.

POST /api/v1/admin/tags 🔒 Auth required

Create a tag. Responds 201.

Request body fields
FieldTypeRequiredRules
namestringYesMax 100
slugstringYesLowercase letters, digits, - and _ only. Max 100. Unique
PUT /api/v1/admin/tags/{id} 🔒 Auth required

Update a tag. Full-representation: name and slug are required on every call.

Request body fields
FieldTypeRequiredRules
namestringYesMax 100
slugstringYesLowercase letters, digits, - and _ only. Max 100. Unique (ignores own record)
PATCH /api/v1/admin/tags/{id} 🔒 Auth required

Identical to PUT — same handler, same required fields.

Request body fields
FieldTypeRequiredRules
namestringYesMax 100
slugstringYesLowercase letters, digits, - and _ only. Max 100. Unique (ignores own record)
DELETE /api/v1/admin/tags/{id} 🔒 Auth required

Delete a tag. Unlike categories this always succeeds: the tag is detached from every article first rather than refused.

Admin — Affiliates

GET /api/v1/admin/affiliates 🔒 Auth required

Paginated list ordered by name, 20 per page — not the full set in one response. Each entry carries id, name, slug, url, click_count, created_at and updated_at.

POST /api/v1/admin/affiliates 🔒 Auth required

Create an affiliate link. Responds 201 with click_count at 0.

Request body fields
FieldTypeRequiredRules
namestringYesMax 255
slugstringYesLowercase letters, digits, - and _ only. Max 255. Unique. This is the value [affiliate slug="..."] shortcodes reference
urlstringYesMax 2048. Scheme must be http or https — this value is handed straight to the browser
PUT /api/v1/admin/affiliates/{id} 🔒 Auth required

Update an affiliate link. Full-representation: name, slug and url are required on every call.

Request body fields
FieldTypeRequiredRules
namestringYesMax 255
slugstringYesLowercase letters, digits, - and _ only. Max 255. Unique (ignores own record). This is the value [affiliate slug="..."] shortcodes reference
urlstringYesMax 2048. Scheme must be http or https — this value is handed straight to the browser
PATCH /api/v1/admin/affiliates/{id} 🔒 Auth required

Identical to PUT — same handler, same required fields.

Request body fields
FieldTypeRequiredRules
namestringYesMax 255
slugstringYesLowercase letters, digits, - and _ only. Max 255. Unique (ignores own record). This is the value [affiliate slug="..."] shortcodes reference
urlstringYesMax 2048. Scheme must be http or https — this value is handed straight to the browser
DELETE /api/v1/admin/affiliates/{id} 🔒 Auth required

Delete an affiliate link and its recorded clicks. Returns 409 while any article body still embeds an [affiliate] shortcode for its slug, since deleting would leave those articles pointing at a dead /out/{slug} link — the message names how many.

Admin — Newsletter

GET /api/v1/admin/newsletter 🔒 Auth required

Paginated list of newsletter subscribers (15 per page), newest first. Each entry carries id, email and created_at.

GET /api/v1/admin/newsletter/export 🔒 Auth required

Exports all newsletter subscribers as a downloadable CSV. This is the one admin endpoint that does not return the JSON envelope: the response is text/csv; charset=utf-8 with Content-Disposition: attachment; filename=subscribers-YYYY-MM-DD.csv.

DELETE /api/v1/admin/newsletter/{id} 🔒 Auth required

Permanently removes a subscriber.

Admin — Messages

GET /api/v1/admin/messages 🔒 Auth required

Paginated list of contact form messages (15 per page), newest first. Each entry carries id, name, email, message, is_read and created_at.

PATCH /api/v1/admin/messages/{id}/toggle 🔒 Auth required

Flips is_read and returns the updated message. It toggles rather than sets, so there is no way to force a particular state — send it twice and you are back where you started. PATCH only; PUT is not routed.

DELETE /api/v1/admin/messages/{id} 🔒 Auth required

Permanently deletes a contact message.

Admin — Settings

GET /api/v1/admin/settings 🔒 Auth required

Retrieve all website settings (including non-public administration settings).

POST /api/v1/admin/settings 🔒 Auth required

Update website settings. Supports JSON or multipart/form-data for logo and favicon image uploads.

PUT /api/v1/admin/settings 🔒 Auth required

Same handler as POST. Use POST when uploading files, since multipart bodies are not parsed on PUT.

Request body fields (POST / PUT)
FieldTypeRequiredRules
site_namestringYesMax 255. Required on every call, even one that only changes the favicon
site_descriptionstringNoMax 5000
ga_idstringNoMax 255
contact_emailstringNoValid email, max 255
site_logofileNoJPG, JPEG, PNG or WebP. Max 2 MB. multipart/form-data. No SVG — it is XML, can carry script, and would be served from this origin
site_faviconfileNoJPG, JPEG, PNG, WebP or ICO. Max 512 KB. multipart/form-data
remove_site_logobooleanNoClears the uploaded logo and reverts to the bundled asset
remove_site_faviconbooleanNoClears the uploaded favicon and reverts to the bundled asset

Both verbs return the same payload, and every key is always present — an unset setting comes back as "" rather than being omitted. site_logo and site_favicon are the stored paths on the public disk; the accompanying site_logo_url and site_favicon_url are absolute URLs for display, falling back to the bundled assets when nothing has been uploaded.

Admin — Profile

GET /api/v1/admin/profile 🔒 Auth required

The authenticated admin account: id, name, email and created_at. No password field is ever returned.

PUT /api/v1/admin/profile 🔒 Auth required

Update name and email, and optionally the password. Full-representation: name and email are required on every call. PATCH is not routed. A wrong current_password is a 422 carrying errors.current_password. Changing the password revokes every other token on the account — the token making the call survives, all others stop working immediately.

Request body fields
FieldTypeRequiredRules
namestringYesMax 100
emailstringYesValid email, max 255, unique among users (ignores own record)
current_passwordstringOnly with new_passwordThe account's existing password
new_passwordstringNoMax 72 — bcrypt hashes only the first 72 bytes. Must satisfy the app's default password rules and be confirmed
new_password_confirmationstringOnly with new_passwordMust match new_password

Admin — Stats

GET /api/v1/admin/stats 🔒 Auth required

Dashboard summary metrics.

Response example
{ "success": true, "data": { "total_articles": 42, "published_articles": 38, "total_views": 128450, "total_subscribers": 312, "total_messages": 27, "unread_messages": 5, "affiliate_links": 12, "affiliate_clicks_today": 14, "affiliate_clicks_total": 9820 }, "message": "" }

Weekend Wanderer API v1 — Generated 2026