Global Search API
One search box, four result groups. A single endpoint searches posts, people, communities and courses, returning either a short preview of every group (the "All" tab) or a longer list for one group.
- Base URL:
/api/v1 - Auth: required โ
Authorization: Bearer {token}. There is no public search. - Pagination: none. Results are capped lists; there is no
page/per_pageand no paginator envelope. Uselimitto ask for more rows.
Replaces
GET /api/v1/feed/search, which no longer exists. Post search moved here unchanged โ same post object, same visibility rules.
Response envelope
Success { "status": "success", "message": "โฆ", "data": { } }
Error { "status": "error", "message": "โฆ", "data": { } }
Status codes
| Code | Meaning |
|---|---|
| 200 | OK |
| 401 | Missing/invalid token |
| 422 | Validation error (no term, both terms, unknown tab, limit over 50, h with a non-post tab) |
| 500 | Server error |
The endpoint
GET /api/v1/search
GET /api/v1/search?q=laravel โ all four tabs, 5 rows each
GET /api/v1/search?q=laravel&limit=10 โ all four tabs, 10 rows each
GET /api/v1/search?q=laravel&tab=user โ people only, 20 rows
GET /api/v1/search?q=laravel&tab=post&sort=recent โ posts only, newest first
GET /api/v1/search?q=laravel&tab=course&scope=member โ only courses the caller enrolled in
GET /api/v1/search?h=laravel โ posts carrying #laravel
GET /api/v1/search?h=%23laravel โ same (a leading '#' is ignored)
Parameters
| Param | Type | Required | Notes |
|---|---|---|---|
q |
string | one of | Free-text term, 1โ100 chars. Matched with LIKE |
h |
string | one of | Hashtag, with or without #, 1โ100 chars. Posts only |
tab |
enum | no | post ยท user ยท community ยท course. Omit in q mode to search all four |
limit |
int | no | Rows per tab. Default 5 without tab, 20 with one. Max 50 |
sort |
enum | no | Post tab only โ top (engagement-ranked, default) ยท recent (newest first) |
scope |
enum | no | Course & community tabs only โ all (default) ยท created ยท member ยท public |
q and h are mutually exclusive: sending both is a 422, sending neither is a 422.
The two modes
q โ free-text
Matches with LIKE '%term%', per tab:
| Tab | Columns matched |
|---|---|
post |
post body |
user |
full_name, username, bio, and the headline on either role profile |
community |
name, tagline |
course |
title, subtitle, description |
Wildcard characters in the term (%, _) are escaped, so they match literally rather than
widening the search.
h โ hashtag
Returns posts carrying exactly this hashtag. Tag names are normalised the same way the
feed stores them (leading # stripped, lowercased), so #Laravel, Laravel and laravel
are one and the same tag.
h always describes the post tab โ it may be sent with tab=post or with no tab at all,
but pairing it with any other tab is a 422.
The two modes are not interchangeable. A post whose body reads "devlife is a state of mind" is a
q=devlifehit but not anh=devlifeone โ it never carried the tag. A post reading "shipping today #devlife" is a hit in both, because the tag text is part of the body.
Response shape
data.results is always an object keyed by tab โ all four keys when no tab was given,
exactly one when there was. Clients parse one shape either way.
All tabs โ GET /api/v1/search?q=laravel
{
"status": "success",
"message": "Search results retrieved successfully.",
"data": {
"mode": "q",
"query": "laravel",
"tab": null,
"results": {
"post": [ /* post objects, up to `limit` */ ],
"user": [ /* user objects, up to `limit` */ ],
"community": [ /* community objects, up to `limit` */ ],
"course": [ /* course objects, up to `limit` */ ]
}
}
}
One tab โ GET /api/v1/search?h=laravel
{
"status": "success",
"message": "Search results retrieved successfully.",
"data": {
"mode": "h",
"query": "laravel",
"tab": "post",
"results": {
"post": [ /* post objects, up to `limit` */ ]
}
}
}
| Field | Notes |
|---|---|
mode |
q or h โ which parameter drove the search |
query |
The term that was searched, trimmed. For h, as the client sent it |
tab |
The single tab searched, or null when all four were |
results |
Object keyed by tab; each value is a plain array of rows |
What each tab returns
post
The same post object as the feed โ see Public Feed for its full field list โ
and the same visibility rules: the caller's own posts and their connections' posts (any
visibility) plus all public posts. A stranger's connections-only post never appears.
Ordered by sort: top ranks on the engagement hot_score, recent on created_at.
user
{
"id": "9b1cโฆ-uuid",
"name": "Ada Lovelace",
"username": "ada",
"avatar_url": "https://โฆ/avatar.jpg",
"connection_status": "connected",
"is_connected": true,
"bio": "Analytical engines, mostly.",
"headline": "Senior Engineer @ Acme",
"role": "mentor"
}
connection_status is the caller's state toward this person โ none, pending_outgoing,
pending_incoming, or connected โ so a result row can render a Connect button directly.
headline comes from whichever role profile the person has.
Deactivated and banned accounts are never returned, and the caller never matches themselves.
Ranking puts an exact username/name match first, then a prefix match, then everything
else (a hit found only in the bio or headline).
Searching mentors by expertise area, price, rating, language or availability is a different surface โ that is the mentee-facing mentor listing (
GET /api/v1/mentee/mentors) with its filter panel. Global search finds a person by who they are; it is not a filtered catalogue browse.
community
The community summary object โ id, name, slug, tagline, status,
pricing_type (+ label), billing_interval, price, currency, member_count,
cover_url, icon_url, mentor โ plus two fields describing the caller's relationship:
{
"id": "9b1cโฆ-uuid",
"name": "The Laravel Guild",
"slug": "the-laravel-guild",
"tagline": "Ship faster, together",
"status": "published",
"pricing_type": "subscription",
"pricing_type_label": "Subscription",
"billing_interval": "monthly",
"price": "19.00",
"currency": "USD",
"member_count": 412,
"cover_url": "https://โฆ/cover.jpg",
"icon_url": null,
"mentor": { "id": "โฆ", "name": "Ada Lovelace", "username": "ada", "headline": "โฆ", "average_rating": "4.90", "avatar_url": "โฆ" },
"is_creator": false,
"member_role": "member"
}
| Field | Notes |
|---|---|
is_creator |
The caller owns this community |
member_role |
owner ยท member, or null when the caller has no active membership |
Ordered by member_count descending.
course
The standard course object โ see Courses โ plus:
| Field | Notes |
|---|---|
is_creator |
The caller authored this course |
is_enrolled |
The caller holds an enrollment on it |
Ordered by enrollment_count descending.
Visibility: what the caller can find
Search never leaks anything. Beyond the post rules above:
People โ active, non-banned accounts only; the caller is excluded from their own results.
Courses โ the union of:
| Arm | Rule |
|---|---|
| public | status = published and visibility = public |
| created | authored by the caller โ any status, drafts included |
| enrolled | the caller holds an enrollment โ any status or visibility, so a community_only course they paid for is findable |
Communities โ the union of:
| Arm | Rule |
|---|---|
| public | status = published |
| created | owned by the caller โ any status, drafts included |
| joined | the caller holds an active membership โ any status |
Another mentor's draft course or unpublished community is never returned.
Narrowing with scope
scope restricts the course and community tabs to a single arm โ the tab UI for "My courses"
/ "My communities" needs no separate endpoint:
| Value | Returns |
|---|---|
all (default) |
The full union above |
created |
Only what the caller authored/owns |
member |
Only what the caller enrolled in / joined |
public |
Only the published, publicly-listed rows |
scope is ignored by the post and user tabs. An arm the caller has no claim to
(e.g. scope=created for someone who is not a mentor) returns an empty list, never
everything.
Errors
| Request | Result |
|---|---|
(no q, no h) |
422 โ Provide either a search term (q) or a hashtag (h). |
?q=a&h=b |
422 โ Search by term (q) or by hashtag (h), not both. |
?q=a&tab=posts |
422 โ unknown tab (note the tabs are singular: post, not posts) |
?q=a&limit=51 |
422 โ limit may not exceed 50 |
?h=tag&tab=user |
422 โ A hashtag search only covers the post tab. |
| (no token) | 401 |
Notes for clients
- Tab counts. The response carries no totals โ a capped list of
limitrows is all you get. If a tab returns exactlylimitrows, assume there may be more and re-request that tab with a largerlimit(up to 50). - Two calls, not five. Render the All view from one
?q=call, then re-request a single tab with?tab=when the user opens it. Do not fan out one call per tab. - Tapping a hashtag in any post should search
?h={tag}, not?q=#{tag}โ the former finds posts that carry the tag, the latter finds posts that merely mention the text.