Lewati ke konten utama

API Endpoints

All backend routes exposed by the Seken NestJS + Fastify backend. Intended for developers building or maintaining the web and mobile clients.

Backend Overview​

The backend is a single NestJS 10 + Fastify service with Drizzle ORM, deployed as a Docker container on Dokploy. Route groupings below map to NestJS controller modules.

Route groupPurposeBase URL
Main APIListings, orders, shipping, AI, auth, notifications, push, webhookshttps://api.pasarseken.id (prod)
Admin routesInternal ops panel API (/admin/*)Same service, role-gated
Cuan deliveryOutbox → cuan webhook delivery (in-process scheduler via @nestjs/schedule)Same service

All authenticated routes require two headers: Authorization: Bearer <anon-key> and X-User-Token: <user-jwt> (see Authentication below).


Main API​

Health​

MethodPathDescription
GET/healthReturns { ok: true, version: "..." }, used by uptime monitoring

Auth​

MethodPathDescription
POST/auth/registerCreate user profile after Supabase Auth signup
GET/auth/meGet current user profile
PATCH/auth/meUpdate profile (bio, photo, location)

Listings​

MethodPathDescription
GET/listingsList all active listings (paginated, filterable)
GET/listings/:idGet single listing by ID
POST/listingsCreate a new listing
PATCH/listings/:idUpdate a listing (seller only)
DELETE/listings/:idSoft-delete a listing (seller only)
GET/listings/user/:userIdGet all listings by a user
GET/listings/boostedGet the ranked pool of boosted and organic listings (paginated, stable sort)

GET /listings, Query Parameters (PS-233)​

ParameterTypeDefaultDescription
limitinteger50Maximum number of listings to return
offsetinteger0Number of listings to skip (for pagination)
genderstring,Filter by gender (e.g. male, female, unisex)
product_categorystring,Filter by product category slug
sortBystring,Sort order: newest, lowest_price, or highest_price

GET /listings, Response Shape (PS-233)​

Before PS-233 this endpoint returned a bare KVListing[] array. It now returns a paginated envelope:

{
"listings": [...],
"total": 120,
"offset": 0,
"limit": 50
}
FieldTypeDescription
listingsKVListing[]The page of results
totalintegerTotal number of listings matching the query (across all pages)
offsetintegerThe offset value used for this request
limitintegerThe limit value used for this request

Detecting more pages: hasMore is not included in the response body. Callers compute it as:

hasMore = offset + listings.length < total

Breaking change: Clients that treated the response as a direct array must be updated to read response.listings instead.

Flutter and web clients updated in PS-233. If you maintain a third-party integration against this endpoint, update your response parsing before deploying against the latest backend.

GET /listings/boosted, Query Parameters (PS-234)​

ParameterTypeDefaultMaxDescription
limitinteger2450Maximum number of listings to return per page
offsetinteger0,Number of listings to skip (for pagination)

GET /listings/boosted, Response Shape (PS-234)​

Before PS-234 this endpoint performed a non-deterministic shuffle and returned a single page with no pagination support. It now returns a stable, paginated envelope sorted by boost score descending, with listing id as the tiebreaker.

{
"listings": [...],
"total": 84,
"offset": 0,
"limit": 24
}
FieldTypeDescription
listingsKVListing[]The page of results, sorted by score desc then id asc
totalintegerTotal pool size before slicing, i.e. the full number of boosted + organic listings available, not the length of the returned page
offsetintegerThe offset value used for this request
limitintegerThe limit value used for this request

Detecting more pages: hasMore is not included in the response body. Callers compute it as:

hasMore = offset + listings.length < total

Breaking changes in PS-234:

  • Response key renamed: boosted → listings. Clients reading response.boosted must switch to response.listings.
  • Fields removed: promoted_count and organic_count are no longer present in the response.
  • total semantics changed: it now reflects the full pool size before pagination, not the number of items returned in the current page.

Flutter and web clients updated in PS-234. If you maintain a third-party integration against GET /listings/boosted, update your response parsing, the old boosted key and promoted_count/organic_count fields no longer exist.

React web, Infinite scroll implementation (PS-235)​

PS-235 is a consumer-side change, no new backend endpoints. The React home page (DesktopHome.tsx) was updated to use the paginated envelopes introduced in PS-233 and PS-234 and to replace the manual "Muat Lebih Banyak" load-more button with an IntersectionObserver-based infinite scroll.

use-boosted-listings.ts

Reads .listings from the GET /listings/boosted response (PS-234 envelope). Exposes three new values to callers:

ValueTypeDescription
loadMore()functionFetches the next page by incrementing offset
isLoadingMorebooleantrue while the next-page request is in flight
hasMorebooleanDerived as total &gt; 0 && offset &lt; total

use-listings.ts

Unwraps the GET /listings paginated envelope (PS-233) internally. Existing callers receive the same KVListing[] shape they always did, no call-site changes required.

DesktopHome.tsx, sentinel pattern

An IntersectionObserver watches a zero-height sentinel div rendered below the listing grid. When the sentinel enters the viewport (configured with rootMargin: "200px", so the observer fires 200 px before the element reaches the fold), loadMore() is called automatically. A Loader2 spinner is shown below the grid while isLoadingMore is true. The observer is disconnected and not re-attached when hasMore is false, preventing over-fetching.

Flutter parity gap (resolved in PS-236)

The Flutter boosted datasource previously read the old boosted key from the GET /listings/boosted response. This gap is closed by PS-236, see the section below.

Flutter mobile, Infinite scroll implementation (PS-236)​

PS-236 is a consumer-side change, no new backend endpoints. It wires infinite scroll on the Flutter home screen, fixes both the PS-233 and PS-234 API contract changes in the Flutter datasource layer, and closes the Flutter parity gap flagged in PS-235.

New types

TypeLayerShape
ListingsPageDatasource boundary{ listings, total, offset, limit }, mirrors the paginated envelope returned by the backend
ListingsPageResultDomain layer (Equatable)Wraps ListingsPage for use in providers and use cases

API contract fixes

Datasource methodWhat changed
getListingsUnwraps response['listings'] and response['total'] from the PS-233 paginated envelope (was reading a bare array)
getBoostedListingsReads the listings key (was boosted): PS-234 fix. promoted_count and organic_count reads removed.

Offline cache fallback

When the device is offline and the datasource falls back to cache, total is synthesised as cached.length. This ensures hasMore evaluates to false while offline, preventing the provider from attempting further page loads against an unavailable network.

Infinite scroll wiring

ListingsProvider drives infinite scroll with the following logic:

ConcernImplementation
hasMore formula_offset &lt; page.total, replaces the previous broken check of length &gt;= pageSize
Error stateisLoadingMoreError flag is set on a failed loadMore() call
RetryCalling loadMore() resets isLoadingMoreError before attempting the next fetch
Scroll guardloadMore() is skipped when isLoadingMoreError is true, preventing the scroll listener from hammering a failing endpoint

home_screen.dart footer

The footer below the listing grid renders one of three states:

StateUI shown
Loading next pageSpinner
Error on last load"Tap untuk coba lagi" retry button
No more pages / idleHidden (zero-height)

Pagination feature complete

PS-236 is the final ticket in the four-part pagination series:

TicketScope
PS-233Backend: GET /listings paginated envelope
PS-234Backend: GET /listings/boosted stable pagination
PS-235React web: infinite scroll via IntersectionObserver
PS-236Flutter mobile: infinite scroll + API contract fixes

All four tickets are shipped. The pagination feature is complete end-to-end across backend, web, and mobile.

Orders​

MethodPathDescription
POST/ordersCreate an order and generate Xendit invoice
GET/orders/:idGet order details
GET/orders/myGet all orders for current user (buyer + seller)
PUT/orders/:id/statusAdvance order status (state-machine transition)
PUT/orders/:id/shipSeller marks item as shipped + enters tracking number
PUT/orders/:id/confirmBuyer confirms receipt → triggers escrow release
PUT/orders/:id/disputeBuyer opens a dispute
PUT/orders/:id/cancelCancel an order (within cancellation window)
POST/orders/:id/return-requestBuyer requests a return
PUT/orders/:id/return-approveSeller/admin approves a return
PUT/orders/:id/return-receivedSeller marks the returned item received
PUT/orders/:id/return-rejectSeller/admin rejects a return

Order state-transition actions use PUT; order creation (POST /orders) and return-request (POST /orders/:id/return-request) use POST. There are no PATCH order routes. The Xendit payment webhook is listed under Webhooks, not here.

For payment flows and escrow behaviour, see Payments & Buyer Protection →. For fee configuration (0% seller commission, buyer protection fee), see Architecture →.

Shipping​

MethodPathDescription
POST/shipping/ratesGet shipping cost estimates from Biteship
POST/shipping/bookBook a shipment (generate airway bill)
GET/shipping/track/:trackingNumberGet live tracking status
GET/shipping/couriersList available couriers

For courier options and user-facing shipping flow, see Shipping & Delivery →.

AI Vision​

MethodPathDescription
POST/ai/categorizeSend a listing photo (HTTPS URL) to the vision model → returns category, brand, condition, and 20+ structured fields

Model: gpt-4o-mini (overridable via the OPENAI_VISION_MODEL env var). This route powers the 60-second AI listing flow. See How to Sell → for the user perspective.

In-App Notifications​

Notifications are stored in the notifications table and polled by clients (React every 30 s, Flutter on screen mount).

MethodPathDescription
GET/notificationsList all notifications for the current user (read + unread, newest first)
PUT/notifications/read-allMark all notifications as read
PUT/notifications/:id/toggle-readToggle read state of a single notification
GET/notifications/preferencesGet user notification preferences
PUT/notifications/preferencesUpdate notification preferences (muted categories, email digest, push enabled)

Push Notifications​

VAPID-based Web Push. Subscriptions are stored in the push_subscriptions table.

MethodPathDescription
GET/push/vapid-public-keyGet the VAPID public key for client-side subscription setup (no auth required)
POST/push/subscribeRegister a device push subscription
DELETE/push/unsubscribeRemove a push subscription (soft-delete)
GET/push/subscriptionsList the current user's active push subscriptions
POST/push/testSend a test push notification to all of the current user's subscribed devices

KTP Verification​

MethodPathDescription
POST/ktp/submitUpload KTP photo + selfie for verification
GET/ktp/statusGet verification status for current user

For verification flow from user perspective, see KTP Verification →.

Reviews​

MethodPathDescription
POST/reviewsSubmit a review after order completion
GET/reviews/user/:userIdGet reviews for a user

Ops Panel API (/admin/*)​

Internal only. Requires admin JWT. Used by the Ops Panel dashboard.

MethodPathDescription
GET/admin/analyticsMAU, transactions, GMV, revenue
GET/admin/usersList all users
GET/admin/ordersList all orders with filters
GET/admin/ktp/pendingKTP submissions pending review
POST/admin/ktp/:id/approveApprove a KTP submission
POST/admin/ktp/:id/rejectReject a KTP submission
GET/admin/disputesList open disputes
POST/admin/disputes/:id/resolveResolve a dispute (release or refund)

Webhooks​

Inbound callbacks from payment, logistics, and email providers. All live in the main NestJS service and are authenticated by provider signature/token verification, not the user dual-header scheme.

MethodPathDescription
POST/webhooks/xenditXendit payment callback (invoice paid, expired, failed)
POST/webhooks/xendit/payoutXendit payout/disbursement status callback
POST/webhooks/xendit-nativeXendit native (non-invoice) payment callback
POST/webhooks/biteshipBiteship tracking update callback (shipment status changes)
POST/webhooks/resendResend email delivery callback (Svix-signed)

Request / Response Conventions​

Authentication​

All authenticated routes require both headers, the two-header invariant (PS-798):

Authorization: Bearer <supabase-anon-key>
X-User-Token: <user-jwt>

Authorization carries the platform anon key (public; used for gateway-level validation). X-User-Token carries the SEKEN JWT that identifies the caller and is verified by the NestJS auth guard. Both are required: a request missing either header is rejected with 401. The earlier "Authorization Bearer fallback" was removed as part of the P2.1 security hardening. Webhook routes are exempt (they use provider signature verification).

Standard Success Response​

{
"ok": true,
"data": { ... }
}

Standard Error Response​

{
"ok": false,
"error": {
"code": "NOT_FOUND",
"message": "Listing not found"
}
}

Error Codes​

CodeHTTP StatusMeaning
VALIDATION400Missing or invalid input
UNAUTHORIZED401No valid JWT
FORBIDDEN403Action not allowed for this user
NOT_FOUND404Resource does not exist
CONFLICT409Duplicate or state conflict
INTERNAL500Unexpected server error

Deployment​

Backend deploys automatically via CI on merge to develop (staging) or main (production). CI builds the Docker image, pushes to DOCR, and signals Dokploy to pull and restart.

See Deployment → for the full deploy process. See Operations: Deployment Guide → for the deploy checklist and rollback steps.