Requests Endpoints
Community Q&A — supports both private 1-to-1 supporter chats AND public many-to-many discussions. See Requests feature for the conceptual model.
Requests CRUD
GET /api/requests
Browse requests with filters.
Query: category?, mine=true, helped=true, status?, lat?, lng?, cursor?, limit=20
Response: { requests: Request[], nextCursor } — each includes consumer, _count.threads, optional distance (computed if lat/lng provided)
POST /api/requests 🔒
Create a request.
Body:
{
title: string;
description?: string;
category: string; // one of 35 — see CATEGORIES in front-end
phone?: string; // optional, hidden if isAnonymous
location?: string;
latitude?: number;
longitude?: number;
isAnonymous?: boolean;
discussionType?: "private" | "public"; // default: "private"
}
Response: { request: Request, broadcastResult: { providers, consumers } } — broadcast count tells you how many people got the push notification.
GET /api/requests/[id]
Single request detail.
PATCH /api/requests/[id] 🔒
Update status (typically status='completed' to close).
DELETE /api/requests/[id] 🔒
Owner soft-delete.
Threads (the gnarly bit)
GET /api/requests/[id]/threads 🔒
List threads for a request.
- For
privaterequests: returns up to 3 threads, one per supporter. - For
publicrequests: returns the SINGLE shared thread (lookup-or-create idempotent).
Response: { threads: RequestThread[] }
POST /api/requests/[id]/threads 🔒
Open a thread.
- For
privaterequests: creates a new supporter thread. Rejects ifMAX_THREADS_PER_REQUEST=3reached. Rejects the request OWNER (you can't support your own request). - For
publicrequests: returns the existing shared thread (lookup-or-create). Doesn't reject the owner.
The RequestThread.supporterId for public-mode threads is set to the request OWNER as a schema compromise (avoids needing a polymorphic field). The public UI never reads this value.
Messages
GET /api/requests/[id]/threads/[threadId]/messages 🔒
Returns thread messages.
- Private thread: only the requester + the supporter can read (others get 403)
- Public thread: anyone can read (gates the 403 on
!isPublic)
Response: { messages: RequestMessage[] }
POST /api/requests/[id]/threads/[threadId]/messages 🔒
Post a message.
Body: { text } (max 500 chars for public, no enforced limit for private)
For private threads: triggers notifyThreadReply (push notification to the other participant). For public threads: skips the notification (would spam every participant).
PATCH /api/requests/[id]/threads/[threadId]/messages/[messageId] 🔒
Edit a message. Sender only. 15-min window for public threads.
Body: { text }
Response: { message } with isEdited: true, editedAt
DELETE /api/requests/[id]/threads/[threadId]/messages/[messageId] 🔒
Soft-delete (isDeleted: true). Sender or request owner (in public threads). 7-day closure window.
Common shapes
Request
{
id: number;
title: string;
description: string | null;
category: string;
phone: string | null;
status: "active" | "completed" | "deleted";
location: string | null;
latitude: number | null;
longitude: number | null;
isAnonymous: boolean;
discussionType: "private" | "public"; // default "private"
consumerId: number;
consumer: { id, name, alias? };
_count?: { threads: number };
distance?: number | null; // km, computed when lat/lng query provided
createdAt: string;
}
RequestThread
{
id: number;
requestId: number;
supporterId: number; // for public threads, set to request.consumerId (compromise)
consumer: { id, name, alias? };
_count: { messages: number };
createdAt: string;
}
RequestMessage
{
id: number;
threadId: number;
consumerId: number;
text: string;
isEdited: boolean;
editedAt: string | null;
isDeleted: boolean;
consumer: { id, name, alias? };
createdAt: string;
}
Rate limits
- Create request:
RATE_LIMITS.PUBLIC_WRITE(10 per 5 min per IP) - Post message: same
Related
- Requests feature
tests/web-mobile-parity-requests.test.ts— locks the public-discussion UI parity