- seed_demo_data: every seeded Store.logo/cover_image and Product.image now points at a shared placeholder MinIO object_key (uploads/c1508b9e-c01f-4892-b24c-c1a949b5b5f5.png) instead of null, so demo data exercises the image_url/logo_url/etc. fields end-to-end. Note: <field>_url will 404 until that object is actually uploaded to the bucket. - .env.example: drop the redundant MINIO_EXTERNAL_ENDPOINT* lines — they already default to MINIO_ENDPOINT/MINIO_USE_HTTPS, and aren't read anywhere in this app's actual presign/read path. - Add MEDIA_INTEGRATION.md: reference doc for the MinIO/presigned-media work on this branch (architecture, settings, API shape, existing-data caveats, what was verified).
32 KiB
Winofy API — Frontend Integration Guide
This document is written for whoever (human or AI assistant) is building the two Winofy clients — اپ مشتری (customer app) and پنل فروشنده (seller panel) — against this backend. It covers auth, conventions, every endpoint, and the end-to-end flows that tie them together. Read §3 (Authentication) and §4 (Conventions) first — everything else assumes them. Jump to §10 for a one-glance "which screen calls which endpoint" table.
Backend repo: winofy-backend (Django 6 + DRF). Swagger UI: GET /api/swagger/swagger-ui/. Raw OpenAPI schema: GET /api/swagger/schema/.
Branch note: this guide describes
master, which does not yet include payment integration (wallet debits, the online gateway, seller payouts/withdrawals) — that's onfeature/payment. Onmaster,checkoutstill accepts apayment_methodand creates orders normally, butOrderGroup.payment_statusjust stayspendingregardless of method, there's nopaymentkey in the checkout response, and theSeller · Walletendpoints (§7) don't exist yet. Everything else in this doc applies to both branches.
1. Base URL
All endpoints below are relative to /api/. Domain apps live under /api/v1/; health check and OAuth2 live directly under /api/.
{API_BASE_URL}/api/v1/...
2. Two clients, one API, one role model
There is no separate "seller API" service — it's the same backend, same auth, same User. Whether a logged-in user is a "customer" or a "seller" is determined entirely by whether they own a Store:
- Any authenticated user can call the customer-facing endpoints (browse, cart, checkout, etc).
POST /api/v1/seller/store/lets any authenticated user become a seller by creating their store (this is the "ایجاد فروشگاه" screen). One user → at most one store.- Every endpoint under
seller/...requires the user to already own a store (403/404otherwise — see §4.3).
So: the seller panel app should call GET /api/v1/seller/store/ right after login; a 404 means "show the create-store onboarding flow," anything else means "show the dashboard."
3. Authentication
This backend does not implement login, OTP, or signup endpoints. Like every other Winsoo/Gooyal service, it's an OAuth2 resource server: your client authenticates directly against the central Gooyal accounts service and sends the resulting bearer token to Winofy.
Authorization: Bearer <access_token issued by Gooyal accounts>
- The phone-number + OTP screens in the Figma file (C01/C02 for customers, S01/S02 for sellers) are a UI over Gooyal's own
/oauth2/token/endpoint (password/OTP grant) — not a Winofy endpoint. Get the exact request shape and base URL from whoever owns the Gooyal accounts service integration for this app. - Winofy will lazily create a local shadow user (keyed by the same UUID Gooyal uses) the first time it sees a valid token for that user — the frontend doesn't need to do anything to "register" a user with Winofy.
- There is no refresh-token handling documented here; follow the standard OAuth2 refresh flow against Gooyal accounts.
3.1 Local development without real Gooyal credentials
If you're integrating against a local instance that doesn't have real Gooyal OAuth2 credentials wired up yet, the two workarounds are:
- Django session auth (also enabled): log into
/api/admin/with a superuser account in a browser, then a browser-based dev client sharing cookies can call the API using that session. Not viable for a mobile app or a separate origin without CORS+CSRF setup. - Ask backend/DevOps for a real Gooyal
client_id/client_secretand a test phone number — this is the only realistic path for end-to-end testing outside a browser.
There is no dev-mode bypass endpoint in this API — don't build against one that doesn't exist.
4. Conventions
4.1 Success responses — plain, not wrapped
Successful responses are the serializer's JSON directly. There is no {success: true, data: ...} envelope — that only applies to errors (§4.2). Don't unwrap a data key that isn't there.
4.2 Error responses — always wrapped, Persian messages
Every non-2xx response has this shape:
{
"success": false,
"status_code": 400,
"status_message": "Bad Request",
"details": {
"message": { "field_name": ["پیام خطا به فارسی"] },
"error": "invalid",
"timestamp": "2026-08-09T12:00:00.000000+00:00"
}
}
details.messageis either a string, or an object keyed by field name (validation errors) — check which shape you got before rendering.- All user-facing error text is in Persian — show
details.messagedirectly to the user, don't re-translate. - Common status codes you'll see:
400(validation),401(missing/invalid token),403(authenticated but not allowed — e.g. non-seller hitting a seller endpoint's permission check),404(not found / not yours),409(conflict — e.g. re-creating a store, invalid status transition),503(an upstream Gooyal/payment-gateway call failed).
4.3 Seller-endpoint permission errors
seller/* endpoints use two different failure modes depending on the view — be ready for either:
403 Forbidden(from theIsStoreOwnerpermission check) on mostseller/*list/action endpoints.404 Not Foundspecifically fromGET/PATCH /seller/store/before a store exists (it does a plainget_object_or_404, not a permission check, since "you don't have a store yet" is the expected first-run state).
4.4 Pagination
List endpoints use limit/offset pagination:
GET /api/v1/stores/?limit=20&offset=40
{ "count": 132, "next": "http://.../stores/?limit=20&offset=60", "previous": "http://.../stores/?limit=20&offset=20", "results": [ ... ] }
Default page size is 50 if limit is omitted.
4.5 Identifiers, money, dates
- Every object's primary key is a UUID string (field name
uuid), not an integer. Use it in URLs:/api/v1/products/{uuid}/. - All money fields (
price,delivery_fee,items_subtotal,total_amount,amount, etc.) are integers in Toman, no decimals, no currency string. - All timestamps are ISO 8601 with timezone (
created_at,placed_at,scheduled_at, ...). - Image fields (
image,logo,cover_image,icon) hold an opaque MinIO object key (ornull), not a URL — read the sibling<field>_url(e.g.logo_url) for a fetchable, presigned, time-limited URL. See §4.7.
4.6 Enums
| Field | Values | Persian label |
|---|---|---|
Address.label |
home, work, other |
خانه, محل کار, سایر |
Product.unit_type |
gram, ml, piece |
گرم, میلیلیتر, عدد |
Store.status |
pending, approved, suspended |
در انتظار تایید, تایید شده, معلق شده |
StoreWorkingHours.weekday |
0–6 |
شنبه=0 … جمعه=6 (Iranian week, not Sun-Sat) |
OrderGroup.delivery_type |
express, scheduled |
ارسال فوری, زمانبندی شده |
OrderGroup.payment_method |
wallet, online, cash_on_delivery |
کیف پول وینسو, درگاه بانکی, پرداخت در محل |
OrderGroup.payment_status |
pending, paid, failed |
در انتظار پرداخت, پرداخت شده, ناموفق |
Order.status |
placed, preparing, ready_to_ship, handed_to_courier, delivered, cancelled |
سفارش ثبت شد → در حال آمادهسازی → آماده ارسال → تحویل سفیر شد → تحویل داده شد (+ لغو شده) |
SellerWithdrawalRequest.status |
pending, processing, paid, rejected |
در انتظار بررسی, در حال پردازش, واریز شده, رد شده |
WalletTransactionRef.direction |
debit, credit |
برداشت, واریز |
Notification.type |
new_order, settlement_done, new_review, order_cancelled |
سفارش جدید, تسویه حساب, نظر جدید, لغو سفارش |
Order.status only moves forward through that exact sequence (or to cancelled from placed/preparing only) — see §6.3. SellerWithdrawalRequest/WalletTransactionRef only exist on feature/payment.
4.7 Image uploads (presigned MinIO)
Every image field (ProductCategory.icon, Product.image, StoreCategory.icon, Store.logo, Store.cover_image) is a MinIO object key, not a Django-served file. Uploading is a two-step flow — the backend never receives the file bytes:
POST /api/media/presign/(auth required) with{"filename": "photo.jpg"}→201 {"upload_url": "...", "object_key": "uploads/<uuid>.jpg"}.upload_urlis a presigned MinIOPUTURL valid for 30 minutes.- Upload the raw file bytes directly to
upload_url(a plainPUT, no auth header, no JSON body — just the file as the request body). - Send the
object_keyfrom step 1 as the field's value in the resource's create/update body (e.g.{"logo": "uploads/<uuid>.jpg", ...}onPATCH /api/v1/seller/store/).
On read, every serializer that has an image field also returns a <field>_url sibling (icon_url, image_url, logo_url, cover_image_url) — a freshly presigned GET URL valid for 1 hour. Always re-fetch the resource (or re-request) rather than caching these URLs past an hour; they expire.
Order.status only moves forward through that exact sequence (or to cancelled from placed/preparing only) — see §6.3.
5. Customer app — endpoint reference
Auth column: Public = no token needed, Auth = any logged-in user, all scoped to the requester unless noted.
Locations (Locations / Addresses in swagger)
| Method | Path | Auth | Notes |
|---|---|---|---|
| GET | /api/v1/cities/ |
Public | List active cities. |
| GET | /api/v1/neighborhoods/?city={uuid} |
Public | List neighborhoods, optionally filtered by city. |
| GET | /api/v1/addresses/ |
Auth | The caller's own saved addresses. |
| POST | /api/v1/addresses/ |
Auth | Create an address (see body below). |
| GET/PATCH/DELETE | /api/v1/addresses/{uuid}/ |
Auth |
Address create/update body:
{
"label": "home",
"title": "",
"full_address": "خیابان ولیعصر، کوچه ۱۲",
"plaque": "15", "floor": "2", "unit": "3",
"recipient_name": "سارا احمدی", "recipient_phone": "09123456789",
"city_uuid": "<city uuid>",
"neighborhood_uuid": "<neighborhood uuid or omit>",
"latitude": 35.7595, "longitude": 51.4088,
"is_default": true
}
Response mirrors this but with city/neighborhood as nested objects (not *_uuid), plus uuid and created_at.
Stores (Stores tag)
| Method | Path | Auth | Notes |
|---|---|---|---|
| GET | /api/v1/store-categories/ |
Public | سوپرمارکت / کافه / رستوران / ... |
| GET | /api/v1/stores/?neighborhood={uuid}&category={uuid}&search=text&lat=..&lng=.. |
Public | Home feed / search. lat+lng sorts by distance (15 km radius). Only status=approved stores are ever returned. |
| GET | /api/v1/stores/{uuid}/ |
Public | Store page — includes working_hours, accepts_wallet/online/cash_on_delivery, description, address, phone_number (fields the list endpoint omits). |
Store list item shape: uuid, name, category{uuid,name,icon,icon_url,order}, logo, logo_url, cover_image, cover_image_url, rating_avg, rating_count, min_order_amount, delivery_fee, free_delivery_threshold, is_open. See §4.7 for icon/logo/cover_image vs. their _url siblings.
Catalog (Catalog tag)
| Method | Path | Auth | Notes |
|---|---|---|---|
| GET | /api/v1/product-categories/ |
Public | Flat shared taxonomy (parent nullable for subcategories). |
| GET | /api/v1/products/?store={uuid}&category={uuid}&search=text |
Public | Store page's product grid / search. Only active products of approved stores. |
| GET | /api/v1/products/{uuid}/ |
Public | Product detail — adds description, category, store (a StoreListSerializer). |
Product list item shape: uuid, name, image, price, unit_type, unit_value, is_active, is_out_of_stock.
Cart (Cart tag) — Auth required for all
| Method | Path | Notes |
|---|---|---|
| GET | /api/v1/cart/ |
Returns the cart grouped by store — see shape below. |
| DELETE | /api/v1/cart/ |
Empties the whole cart. |
| POST | /api/v1/cart/items/ |
Add a product; if it's already in the cart, quantities add together (not replaced). Body: {"product_uuid": "...", "quantity": 2}. |
| PATCH | /api/v1/cart/items/{item_uuid}/ |
Set an absolute quantity: {"quantity": 5}. |
| DELETE | /api/v1/cart/items/{item_uuid}/ |
Remove one line. |
GET /api/v1/cart/ response — this is the shape behind the multi-store-cart UI (C07):
{
"groups": [
{
"store": { "uuid": "...", "name": "سوپرمارکت پارسیان", "...": "..." },
"items": [
{ "uuid": "<cart item uuid>", "product": { "uuid": "...", "name": "شیر کاله", "price": 28500, "...": "..." }, "quantity": 2, "line_total": 57000 }
],
"items_subtotal": 57000,
"delivery_fee": 15000,
"total": 72000,
"meets_minimum_order": true
}
],
"grand_total": 72000
}
meets_minimum_order: falsemeans this store's slice is belowstore.min_order_amount— checkout will reject it (§6.2). Surface this in the cart UI per-store, same as the mock.delivery_feeis already0here if the store'sfree_delivery_thresholdwas met — don't recompute it client-side.
Checkout (Checkout tag) — Auth required
| Method | Path |
|---|---|
| POST | /api/v1/checkout/ |
Body:
{
"address_uuid": "<address uuid>",
"delivery_type": "express",
"scheduled_at": null,
"payment_method": "wallet",
"notes": ""
}
scheduled_atis required (ISO datetime) whendelivery_typeis"scheduled"— omitting it is a400.address_uuidmust belong to the caller, or400.- The cart is split into one
Orderper store and cleared on success. See §6.1–§6.2 for the full flow including coverage checks and payment-method branching.
Response = an OrderGroup object (§5, Orders below):
{
"uuid": "<order group uuid>",
"recipient_name": "...", "recipient_phone": "...", "full_address": "...",
"delivery_type": "express", "scheduled_at": null,
"payment_method": "online", "payment_status": "pending",
"notes": "", "total_amount": 178500,
"orders": [ /* array of Order objects, one per store — see Orders below */ ],
"created_at": "..."
}
On master, that's it — payment_status stays "pending" no matter which payment_method you send; nothing external is contacted. On feature/payment, the response also includes a payment key and payment_status actually reflects what happened:
wallet:payment={"method": "wallet", "status": "paid"}— synchronous, done.online:payment={"method": "online", "status": "pending", "payment_url": "..."}— redirect/open a webview atpayment_url; the gateway calls Winofy back and payment_status updates asynchronously (pollGET /api/v1/order-groups/{uuid}/orGET /api/v1/orders/{uuid}/to see it flip topaid).cash_on_delivery:payment={"method": "cash_on_delivery", "status": "pending"}— nothing to do, collected on delivery.
Orders (Orders tag) — Auth required, scoped to the caller
| Method | Path | Notes |
|---|---|---|
| GET | /api/v1/order-groups/ |
Order history — each entry may bundle several stores' orders from one checkout. |
| GET | /api/v1/order-groups/{uuid}/ |
|
| GET | /api/v1/orders/{uuid}/ |
Single-order tracking (C09) — one store's slice. |
| POST | /api/v1/orders/{uuid}/cancel/ |
Body: {"reason": ""} (optional). Only works while is_cancellable is true. |
Order object shape (nested inside OrderGroup.orders, or standalone from GET /orders/{uuid}/):
{
"uuid": "...", "store": { "...": "StoreListSerializer" }, "status": "preparing",
"items": [
{ "uuid": "...", "product": "<product uuid>", "product_name_snapshot": "شیر کاله", "unit_price_snapshot": 28500, "quantity": 2, "line_total": 57000 }
],
"items_subtotal": 57000, "delivery_fee": 15000,
"commission_amount": 2280, "seller_payout_amount": 54720,
"total_amount": 72000,
"cancel_reason": "", "courier_name": "", "courier_phone": "",
"placed_at": "...", "delivered_at": null,
"status_logs": [ { "from_status": "", "to_status": "placed", "note": "", "created_at": "..." } ],
"is_cancellable": true
}
items[].productis just the product UUID (not expanded) — useproduct_name_snapshot/unit_price_snapshotfor display; they're frozen at order time and won't change even if the product is later edited or deleted.- Drive the order-tracking stepper UI off
status_logs(ordered oldest→newest) or just offstatusdirectly — both are provided. commission_amount/seller_payout_amountare platform-internal — fine to show in a seller-facing context, not normally shown to the customer.
Reviews (Reviews tag)
| Method | Path | Auth | Notes |
|---|---|---|---|
| GET | /api/v1/reviews/?store={uuid} |
Public | Reviews for a store's page. |
| POST | /api/v1/reviews/ |
Auth | Body: {"order_uuid": "...", "rating": 5, "comment": "..."}. Only allowed once order.status == "delivered", and only once per order (400 otherwise — see error messages in §4.2). |
Response adds customer_name, store (uuid), product (uuid, nullable), seller_reply (null until the seller replies).
Notifications (Notifications tag) — Auth required, shared by both apps
| Method | Path | Notes |
|---|---|---|
| GET | /api/v1/notifications/ |
The caller's notifications (works the same whether they're a customer or a seller — it's just "notifications for this user"). |
| GET | /api/v1/notifications/{uuid}/ |
|
| POST | /api/v1/notifications/{uuid}/mark-read/ |
|
| POST | /api/v1/notifications/mark-all-read/ |
Returns 204. |
Shape: uuid, type, title, body, related_order (uuid, nullable), is_read, created_at. All fields are read-only from the client's perspective (server-generated).
6. Key flows
6.1 Location & delivery-coverage flow (the neighborhood-confirmation-sheet)
Stores don't deliver everywhere — each Store has a service_neighborhoods list. The Figma "محلهت رو انتخاب کن" sheet exists because of this. The API enforces it at checkout time, not earlier — the client's job is to react to the failure, not to pre-validate:
- Customer picks/has an
Address(with aneighborhood_uuid) — via the address list, or the map picker +POST /api/v1/addresses/. - Customer browses/adds to cart freely —
GET /api/v1/stores/and cart endpoints do not check coverage. - On
POST /api/v1/checkout/, if any store in the cart doesn't serve the address's neighborhood, the whole checkout fails with400and a message like "فروشگاه «X» به محله انتخابی شما ارسال ندارد." — show the confirmation sheet / prompt the user to pick a different address at that point.
If you want to warn the customer earlier (e.g. on the store page), you'd need to compare the store's service_neighborhoods (not currently exposed on the customer-facing store serializer — ask backend to add it if you need this pre-check) against the customer's selected/default address neighborhood.
6.2 Multi-store cart → checkout
GET /api/v1/cart/→ render one card pergroups[]entry (matches C07).- Block the "ادامه و ثبت سفارش" button per-store (or overall) if any group has
meets_minimum_order: false. POST /api/v1/checkout/once. The backend, inside one transaction:- re-validates minimum order, stock, and neighborhood coverage per store (can still fail here even if the cart screen looked fine, e.g. a race with another customer depleting stock) — handle the
400gracefully; - creates one
OrderGroup+ oneOrderper store, snapshots the address intorecipient_name/recipient_phone/full_addresson the group (so later address edits don't retroactively change past orders); - decrements
Product.stock_quantityand clears the cart; - branches on
payment_method(§5 Checkout).
- re-validates minimum order, stock, and neighborhood coverage per store (can still fail here even if the cart screen looked fine, e.g. a race with another customer depleting stock) — handle the
- Route based on
payment.method:wallet/cash_on_delivery→ go straight to an order-confirmation screen.online→ openpayment.payment_url, then poll or listen for the order group to reachpayment_status: "paid".
6.3 Order status stepper (seller side drives it, customer side reads it)
The seller panel is the only side that advances status. Valid transitions:
placed → preparing → ready_to_ship → handed_to_courier → delivered
placed → cancelled
preparing → cancelled
No other transition is allowed — the backend returns 409 for anything else (e.g. trying to jump from placed straight to delivered, or cancelling after handed_to_courier). See §7 for the seller-side action endpoints. The customer app's POST /orders/{uuid}/cancel/ uses the same rule (only while placed/preparing).
Commission (Order.commission_amount) is fixed at checkout time from the store's rate (4% by default) applied to items_subtotal only — delivery_fee is excluded from the split and goes to the seller, on both branches. On feature/payment only, the delivered transition additionally credits the seller's Gooyal wallet and fires a settlement_done notification — none of this needs client involvement either way. On master, delivered just marks the order delivered; no money moves yet.
6.4 Payment methods, in full (feature/payment only)
master accepts and stores payment_method on checkout but doesn't act on it — payment_status stays pending regardless. The behavior below is what feature/payment adds:
payment_method |
What happens | Client follow-up |
|---|---|---|
wallet |
Debits the customer's Gooyal wallet synchronously during checkout. | None — payment_status is already paid in the checkout response. |
online |
Opens a charge request on the internal ipg gateway. |
Redirect to payment.payment_url; the gateway calls Winofy back asynchronously. Poll the order/order-group for payment_status. |
cash_on_delivery |
Nothing charged now. | Show "پرداخت در محل" confirmation; money changes hands at delivery, outside the API. |
7. Seller panel — endpoint reference
Every endpoint below requires the caller to own a Store (IsStoreOwner), except store creation itself.
Seller · Store
| Method | Path | Notes |
|---|---|---|
| GET | /api/v1/seller/store/ |
404 if the user hasn't created a store yet → show onboarding. |
| POST | /api/v1/seller/store/ |
Creates the store (one per user, 409 on a second attempt). Body below. |
| PATCH | /api/v1/seller/store/ |
Partial update, same body shape. |
| GET | /api/v1/seller/store/working-hours/ |
Returns array of {weekday, opens_at, closes_at, is_closed}. |
| PUT | /api/v1/seller/store/working-hours/ |
Full replace — send all 7 days each time; body is a bare array (or {"working_hours": [...]}). |
Store create/update body:
{
"name": "سوپرمارکت پارسیان",
"category_uuid": "<store category uuid>",
"description": "...",
"phone_number": "09166352131",
"city_uuid": "<city uuid>",
"address": "خیابان آزادی، نبش کوچه مریم",
"latitude": 35.71, "longitude": 51.35,
"service_neighborhood_uuids": ["<neighborhood uuid>", "..."],
"delivery_radius_km": 5,
"min_order_amount": 50000, "delivery_fee": 25000, "free_delivery_threshold": null,
"accepts_wallet": true, "accepts_online": true, "accepts_cash_on_delivery": true,
"is_open": true
}
rating_avg, rating_count, status are read-only (server-computed; status starts pending — there's no admin-approval endpoint in this API yet, that's a manual/admin-panel step today). To set logo/cover_image, run the presign flow (§4.7) first and include the resulting object_keys in this JSON body — e.g. add "logo": "uploads/<uuid>.png".
Seller · Products
| Method | Path | Notes |
|---|---|---|
| GET | /api/v1/seller/products/ |
Only the caller's own products. |
| POST | /api/v1/seller/products/ |
Create (S07 "افزودن محصول"). |
| GET/PATCH/DELETE | /api/v1/seller/products/{uuid}/ |
|
| PATCH | /api/v1/seller/products/{uuid}/stock/ |
Inventory-only quick update (S08): {"stock_quantity": 12}. |
Product body: name, description, image, category_uuid, price, unit_type, unit_value, stock_quantity, low_stock_threshold, is_active. image is a MinIO object key from the presign flow (§4.7), not a file upload. Response adds image_url (presigned, §4.7), sold_count (read-only), is_out_of_stock, is_low_stock (both computed: stock_quantity <= 0, and 0 < stock_quantity <= low_stock_threshold).
Seller · Orders
| Method | Path | Notes |
|---|---|---|
| GET | /api/v1/seller/orders/?status=placed |
status filter matches the S09 tabs; omit for all. |
| GET | /api/v1/seller/orders/{uuid}/ |
S10 detail. |
| POST | /api/v1/seller/orders/{uuid}/confirm/ |
placed → preparing. |
| POST | /api/v1/seller/orders/{uuid}/mark-ready/ |
preparing → ready_to_ship. |
| POST | /api/v1/seller/orders/{uuid}/mark-shipped/ |
ready_to_ship → handed_to_courier. Body may include courier_name/courier_phone. |
| POST | /api/v1/seller/orders/{uuid}/mark-delivered/ |
handed_to_courier → delivered. Triggers settlement (§6.3). |
| POST | /api/v1/seller/orders/{uuid}/cancel/ |
Only from placed/preparing. Body: {"reason": ""}. |
All the action endpoints accept an optional body {"note": "", "courier_name": "", "courier_phone": ""} and return the updated Order object (§5 shape). A 409 means the transition isn't legal from the order's current status (§6.3) — don't just retry, refetch the order and re-render the correct available actions.
Seller · Wallet (feature/payment only — not on master)
| Method | Path | Notes |
|---|---|---|
| GET | /api/v1/seller/wallet/ |
{"withdrawable_balance": 12500000, "pending_settlement": 350000, "recent_transactions": [...]} (S11). |
| GET | /api/v1/seller/wallet/withdrawals/ |
The seller's withdrawal request history. |
| POST | /api/v1/seller/wallet/withdrawals/ |
S12 form: {"amount": 500000, "iban": "IR120120000000012345678", "account_holder_name": "سارا احمدی"}. iban must match ^IR\d{24}$ or 400. |
recent_transactions[] items: uuid, direction ("debit"/"credit"), amount, purpose, status, created_at.
Seller · Analytics
| Method | Path | Notes |
|---|---|---|
| GET | /api/v1/seller/analytics/?period=today |
period ∈ today/week/month (default today). S17 dashboard. |
Response:
{
"period": "today",
"order_count": 12, "total_sales": 2450000,
"cancellation_rate": 4.5, "average_order_value": 204000,
"chart": [ { "bucket": "2026-08-09T09:00:00Z", "sales": 320000 } ],
"top_products": [ { "name": "شیر کم چرب کاله 1 لیتری", "units_sold": 44, "revenue": 1680000 } ]
}
chart[].bucket is hourly when period=today, daily otherwise. top_products is capped at 5, sorted by revenue.
Seller · Reviews
| Method | Path | Notes |
|---|---|---|
| GET | /api/v1/seller/reviews/ |
Reviews on the caller's store. |
| POST | /api/v1/seller/reviews/{uuid}/reply/ |
{"seller_reply": "ممنون از خرید شما"}. |
Payments webhook (feature/payment only — not on master, and not called by either client app)
GET/POST /api/v1/payments/ipg/callback/ is hit by the ipg payment gateway service itself after a customer completes an online payment, not by the frontend. Don't call it directly; it's documented here only so you know it exists and isn't a client-facing endpoint.
8. Local development / testing
- The backend ships a management command that seeds a realistic dataset: 3 cities, 13 neighborhoods, ~19 stores across 6 categories, ~370 products, 30 customers with addresses, 150+ historical orders in every status (placed/preparing/delivered/cancelled), and reviews. Ask backend to run
python manage.py seed_demo_data(or--flushto reset it) against your dev environment before you start wiring up screens — there's no need to hand-create fixtures. - Swagger UI (
/api/swagger/swagger-ui/) is grouped into the same sections as this doc (Media, Locations, Stores, Catalog, Cart, Checkout, Orders, Reviews, Notifications, and theSeller · *groups —Seller · WalletandPaymentsonly appear onfeature/payment) — use it to try requests once you have a token. GET /api/health/needs no auth and is useful as a "is the backend even up" smoke check.
9. Known gaps to flag back to backend if you hit them
- No store-approval endpoint — new stores sit in
status: "pending"until changed via Django admin. - No admin/public endpoint to see a store's
service_neighborhoodsfrom the customer-facing store serializer (needed if you want to pre-warn about delivery coverage before checkout — see §6.1). - The
ipgonline-payment callback contract (/api/v1/payments/ipg/callback/) is a best-effort integration pending confirmation against a liveipgenvironment — if online payments seem to hang inpending, that's the first place to check.
10. Quick reference — screen → endpoint
The same information as §5/§7, but organized by Figma screen code instead of by resource, for wiring up one screen at a time. "Auth" endpoints need a bearer token (§3); "Public" ones don't.
اپ مشتری (Customer app)
| Screen | Endpoint(s) |
|---|---|
| C01/C02 — Login, OTP | None — Gooyal accounts OAuth2 handles this entirely; see §3. |
| C03 / guest-home — Home feed | GET /api/v1/cities/, GET /api/v1/neighborhoods/?city=, GET /api/v1/store-categories/, GET /api/v1/stores/?neighborhood=&category=&search=&lat=&lng= |
| Address list / map picker / address form | GET/POST /api/v1/addresses/, GET/PUT/PATCH/DELETE /api/v1/addresses/{uuid}/ |
| C04 — Store page | GET /api/v1/stores/{uuid}/, GET /api/v1/product-categories/, GET /api/v1/products/?store={uuid}&category= |
| C05 — Product detail | GET /api/v1/products/{uuid}/ |
| C06 — Search | GET /api/v1/products/?search=, GET /api/v1/stores/?search= |
| C07 — Cart | GET/DELETE /api/v1/cart/, POST /api/v1/cart/items/, PATCH/DELETE /api/v1/cart/items/{item_uuid}/ |
| C08 — Checkout | POST /api/v1/checkout/ (§6.2, §6.4) |
| C09 — Order tracking / history | GET /api/v1/order-groups/ (history), GET /api/v1/order-groups/{uuid}/, GET /api/v1/orders/{uuid}/, POST /api/v1/orders/{uuid}/cancel/ |
| C10 — Post-delivery / reviews | POST /api/v1/reviews/ (after delivered), GET /api/v1/reviews/?store= (store page's review list) |
| Notifications (bell icon) | GET /api/v1/notifications/, POST /api/v1/notifications/{uuid}/mark-read/, POST /api/v1/notifications/mark-all-read/ |
پنل فروشنده (Seller panel)
| Screen | Endpoint(s) |
|---|---|
| S01/S02 — Seller login, OTP | None — Gooyal accounts OAuth2; see §3. |
| S04 — Create store | POST /api/v1/seller/store/ |
| S05 — Dashboard | GET /api/v1/seller/store/, GET /api/v1/seller/analytics/?period=today, GET /api/v1/seller/orders/?status=placed |
| S06 — Product management (list) | GET /api/v1/seller/products/ |
| S07 — Add product | POST /api/media/presign/ (§4.7) → upload the file → POST /api/v1/seller/products/ with image = the returned object_key |
| S08 — Inventory | GET /api/v1/seller/products/, PATCH /api/v1/seller/products/{uuid}/stock/ |
| S09 — Order management (status tabs) | GET /api/v1/seller/orders/?status= |
| S10 — Order detail + stepper | GET /api/v1/seller/orders/{uuid}/, then POST .../confirm/, .../mark-ready/, .../mark-shipped/, .../mark-delivered/, .../cancel/ |
| S11/S12 — Wallet, withdrawal | Not on master — feature/payment only (§7 Seller · Wallet). |
| S14 — Store settings | GET/PATCH /api/v1/seller/store/ (logo/cover uploads use the same presign flow as S07), GET/PUT /api/v1/seller/store/working-hours/ |
| S15 — Seller profile | GET /api/v1/seller/store/, GET /api/v1/seller/reviews/ (rating summary) |
| S16 — Notifications | Same shared endpoints as the customer app, above. |
| S17 — Analytics | GET /api/v1/seller/analytics/?period=today|week|month |
| Reviews & replies | GET /api/v1/seller/reviews/, POST /api/v1/seller/reviews/{uuid}/reply/ |