Skip to main content

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 private requests: returns up to 3 threads, one per supporter.
  • For public requests: returns the SINGLE shared thread (lookup-or-create idempotent).

Response: { threads: RequestThread[] }

POST /api/requests/[id]/threads 🔒

Open a thread.

  • For private requests: creates a new supporter thread. Rejects if MAX_THREADS_PER_REQUEST=3 reached. Rejects the request OWNER (you can't support your own request).
  • For public requests: 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
  • Requests feature
  • tests/web-mobile-parity-requests.test.ts — locks the public-discussion UI parity