۰۹ — API Endpoints (Next.js API Routes)
Base:
src/app/api/
همه روتهاRoute Handlerهستند (route.ts)
Format: JSON
Auth: httpOnly cookie (db_access+db_refresh)
ساختار فایلی
src/app/api/
├── auth/
│ ├── otp/send/route.ts
│ ├── otp/verify/route.ts
│ ├── refresh/route.ts
│ └── logout/route.ts
├── boxes/
│ ├── route.ts GET list
│ └── [id]/route.ts GET detail
├── orders/
│ ├── route.ts POST create
│ └── [id]/
│ ├── route.ts GET detail
│ ├── pay/route.ts POST → payment URL
│ ├── cancel/route.ts POST
│ └── apply-discount/route.ts
├── payments/
│ └── zarinpal/
│ └── callback/route.ts
├── me/route.ts
├── vendor/
│ ├── register/route.ts
│ ├── me/route.ts
│ ├── boxes/
│ │ ├── route.ts
│ │ └── [id]/
│ │ ├── route.ts
│ │ ├── publish/route.ts
│ │ └── cancel/route.ts
│ └── orders/
│ ├── route.ts
│ ├── confirm-pickup/route.ts
│ ├── [id]/mark-paid/route.ts
│ └── [id]/cancel/route.ts
├── admin/
│ ├── vendors/
│ │ ├── route.ts
│ │ └── [id]/
│ │ ├── approve/route.ts
│ │ ├── reject/route.ts
│ │ └── suspend/route.ts
│ ├── packages/route.ts
│ ├── packages/[id]/route.ts
│ ├── discount-codes/route.ts
│ ├── discount-codes/[id]/route.ts
│ ├── orders/route.ts
│ └── orders/[id]/refund/route.ts
└── cron/
└── tick/route.ts GET — cron job (Vercel Cron یا node-cron)Auth
POST /api/auth/otp/send
ts
// body
{ phone: "09xxxxxxxxx", purpose: "login_client" | "login_vendor" }
// response 200
{ expires_in: 120 }
// errors
// 429 OTP_RATE_LIMIT — بیش از ۳ بار/ساعتPOST /api/auth/otp/verify
ts
// body
{ phone: "09xxxxxxxxx", code: "12345", purpose: "login_client" | "login_vendor" }
// response 200 — sets httpOnly cookies
{ role: "client" | "vendor", name: string | null }
// errors
// 401 OTP_INVALID
// 401 OTP_EXPIRED
// 429 OTP_MAX_ATTEMPTSPOST /api/auth/refresh
ts
// cookie: db_refresh
// response 200 — new db_access cookie
{ ok: true }POST /api/auth/logout
ts
// clears cookies
// 204Client — Boxes
GET /api/boxes
ts
// query
?district=&min_price=&max_price=&page=1&page_size=20
// response
{
items: [{
id, vendorId, vendorName, vendorSlug, vendorLogoUrl, vendorTrustBadge,
title, category, imageUrl,
originalValueEstimate, salePrice,
quantityRemaining,
pickupStart, pickupEnd,
distanceM: number | null
}],
page, page_size, total
}GET /api/boxes/[id]
ts
{
id, title, description, category, imageUrl,
originalValueEstimate, salePrice, quantityRemaining,
pickupStart, pickupEnd, status,
vendor: { id, name, slug, address, lat, lng, logoUrl, trustBadge }
}Client — Orders
POST /api/orders
ts
// body
{ surpriseBoxId: string, discountCode?: string, paymentMethod: "gateway" | "pay_at_pickup" }
// response 201
{ orderId, pickupCode, totalPrice, discountAmount, paymentMethod }
// errors
// 409 BOX_SOLD_OUT
// 409 PICKUP_WINDOW_CLOSED
// 400 DISCOUNT_INVALIDPOST /api/orders/[id]/pay
ts
// مخصوص paymentMethod=gateway
// response 200
{ paymentUrl: "https://zarinpal.com/pg/StartPay/..." }GET /api/orders/[id]
ts
{
id, orderStatus, paymentStatus, paymentMethod,
pickupCode, totalPrice, discountAmount,
surpriseBox: { title, vendorName, pickupStart, pickupEnd, address },
confirmedAt, pickedUpAt
}POST /api/orders/[id]/apply-discount
ts
// body — قبل از pay
{ code: "WELCOME20" }
// response
{ discountAmount, totalPrice }POST /api/orders/[id]/cancel
ts
// body
{ reason?: string }
// 204
// **فقط وقتی order_status = pending یا awaiting_vendor**
// بعد از اینکه وندور تأیید کرد (confirmed) → 409 CANCEL_NOT_ALLOWED
// errors: 409 CANCEL_NOT_ALLOWEDPayment callback
GET /api/payments/zarinpal/callback
ts
// query از Zarinpal
?Authority=xxx&Status=OK
// 1. verify with Zarinpal API
// 2. payment.status = paid, payment.paidAt
// 3. order.paymentStatus = paid, order.orderStatus = awaiting_vendor ← نه confirmed
// 4. decrement quantityRemaining
// 5. send SMS to VENDOR: «سفارش جدید — لطفاً تأیید کنید»
// 6. redirect → /order/[id] (success) یا /order/[id]?failed=1
// ** مشتری SMS نمیگیرد تا وندور تأیید کند **Client — Profile
GET /api/me
ts
{ id, phone, name, role }PATCH /api/me
ts
// body
{ name?: string }Vendor
POST /api/vendor/register
ts
// body
{ name, category, address, lat, lng, phone, description? }
// response 201
{ vendorId, status: "pending" }GET /api/vendor/me
ts
{ id, name, status, package: { name, commissionRate } | null }POST /api/vendor/boxes
ts
// body
{
title, description?, category, imageUrl?,
originalValueEstimate, salePrice,
quantityTotal, pickupStart, pickupEnd
}
// response 201 — status: draft
// errors: 409 BOX_ALREADY_EXISTS_TODAYPOST /api/vendor/boxes/[id]/publish
ts
// draft → active
// errors: 403 VENDOR_NOT_APPROVEDGET /api/vendor/orders
ts
// query: ?date=2026-06-23
{
items: [{ id, pickupCode, userPhone, orderStatus, paymentMethod, paymentStatus, totalPrice, createdAt }]
}POST /api/vendor/orders/[id]/confirm
ts
// ← تأیید سفارش توسط وندور (awaiting_vendor → confirmed)
// این باید قبل از اطلاعرسانی مشتری انجام شود
// 1. order.orderStatus = awaiting_vendor چک میشود
// 2. order.orderStatus = confirmed
// 3. SMS به مشتری: «باکس شما آماده تحویل است»
// response 200
{ orderId }
// errors: 404, 409 ORDER_ALREADY_CONFIRMED, 403 FORBIDDENPOST /api/vendor/orders/confirm-pickup
ts
// body
{ pickupCode: "ABC123" }
// 1. find order by pickupCode, check vendor owns it
// 2. order.orderStatus = picked_up, order.pickedUpAt
// 3. if pay_at_pickup → paymentStatus = paid
// response 200
{ orderId, userName }
// errors: 404 PICKUP_CODE_NOT_FOUND, 409 ORDER_ALREADY_CONFIRMEDPOST /api/vendor/orders/[id]/cancel
ts
// وندور میتواند سفارش را قبل از picked_up لغو کند
// response 204
// errors: 409 CANCEL_NOT_ALLOWED (اگر picked_up)POST /api/vendor/orders/[id]/mark-paid
ts
// فقط pay_at_pickup
// payment_status → paidGET /api/vendor/stats/summary
ts
// query: ?from=2026-06-01&to=2026-06-23
{ ordersCount, revenue, pickupRate, noShowCount }Admin
GET /api/admin/vendors
ts
// query: ?status=pending&page=1POST /api/admin/vendors/[id]/approve
ts
// body: { packageId?: string }
// vendor.status = approved, vendor.approvedAt
// send SMS to vendor ownerPOST /api/admin/vendors/[id]/reject
ts
// body: { reason: string }CRUD /api/admin/packages
ts
// GET list, POST create, PATCH /[id], DELETE /[id]CRUD /api/admin/discount-codes
ts
// GET list, POST create, PATCH /[id] (toggle isActive)GET /api/admin/orders
ts
// filters: vendorId, status, dateFrom, dateToPOST /api/admin/orders/[id]/refund
ts
// stub MVP — mark payment_status=refunded
// v2: Zarinpal refund APICron
GET /api/cron/tick
ts
// secret header: x-cron-secret
// runs:
// markNoShows() — orders past pickup_end → no_show
// sendReminders() — 25-35 min before pickup_end → SMS
// expireBoxes() — active boxes past pickup_end → expired
// response: { noShows, reminders, expiredBoxes }Format خطا
ts
{
error: {
code: string, // "BOX_SOLD_OUT"
message: string // "آخرین باکس رزرو شد"
}
}Common error codes
| Code | HTTP |
|---|---|
OTP_INVALID | 401 |
OTP_EXPIRED | 401 |
OTP_RATE_LIMIT | 429 |
BOX_SOLD_OUT | 409 |
BOX_ALREADY_EXISTS_TODAY | 409 |
PICKUP_WINDOW_CLOSED | 409 |
DISCOUNT_INVALID | 400 |
PAYMENT_FAILED | 402 |
FORBIDDEN | 403 |
VENDOR_NOT_APPROVED | 403 |
CANCEL_NOT_ALLOWED | 409 |
PICKUP_CODE_NOT_FOUND | 404 |
ORDER_ALREADY_CONFIRMED | 409 |
Pagination (standard)
ts
{ items: T[], page: number, page_size: number, total: number }Rate limits (middleware)
| Endpoint | Limit |
|---|---|
/api/auth/otp/send | 3/hour/phone |
/api/vendor/orders/confirm-pickup | 20/min/vendor |
/api/cron/tick | secret header required |