winofy-backend/FRONTEND_GUIDE.md
Ali Asadi 52adc732fa Initial Winofy backend: marketplace core (no payment integration)
Django 6 + DRF resource server against the Gooyal accounts OAuth2 service,
matching the Winsoo ecosystem's conventions. Covers locations, stores,
catalog, cart, checkout/orders (with the multi-store-cart split and the
status stepper), reviews, and notifications, plus a demo-data seed command.

Payment integration (wallet debits, online gateway, seller payouts) is
intentionally left out here — see feature/payment.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-10 14:03:15 +03:30

28 KiB
Raw Blame History

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.

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 main, which does not yet include payment integration (wallet debits, the online gateway, seller payouts/withdrawals) — that's on feature/payment. On main, checkout still accepts a payment_method and creates orders normally, but OrderGroup.payment_status just stays pending regardless of method, there's no payment key in the checkout response, and the Seller · Wallet endpoints (§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/404 otherwise — 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:

  1. 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.
  2. Ask backend/DevOps for a real Gooyal client_id/client_secret and 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.message is 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.message directly 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 the IsStoreOwner permission check) on most seller/* list/action endpoints.
  • 404 Not Found specifically from GET/PATCH /seller/store/ before a store exists (it does a plain get_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) are either null or an absolute/relative media URL — never assume they're set.

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.

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,order}, logo, cover_image, rating_avg, rating_count, min_order_amount, delivery_fee, free_delivery_threshold, is_open.

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: false means this store's slice is below store.min_order_amount — checkout will reject it (§6.2). Surface this in the cart UI per-store, same as the mock.
  • delivery_fee is already 0 here if the store's free_delivery_threshold was 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_at is required (ISO datetime) when delivery_type is "scheduled" — omitting it is a 400.
  • address_uuid must belong to the caller, or 400.
  • The cart is split into one Order per 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 main, 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 at payment_url; the gateway calls Winofy back and payment_status updates asynchronously (poll GET /api/v1/order-groups/{uuid}/ or GET /api/v1/orders/{uuid}/ to see it flip to paid).
  • 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[].product is just the product UUID (not expanded) — use product_name_snapshot/unit_price_snapshot for 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 off status directly — both are provided.
  • commission_amount/seller_payout_amount are 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:

  1. Customer picks/has an Address (with a neighborhood_uuid) — via the address list, or the map picker + POST /api/v1/addresses/.
  2. Customer browses/adds to cart freely — GET /api/v1/stores/ and cart endpoints do not check coverage.
  3. On POST /api/v1/checkout/, if any store in the cart doesn't serve the address's neighborhood, the whole checkout fails with 400 and 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

  1. GET /api/v1/cart/ → render one card per groups[] entry (matches C07).
  2. Block the "ادامه و ثبت سفارش" button per-store (or overall) if any group has meets_minimum_order: false.
  3. 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 400 gracefully;
    • creates one OrderGroup + one Order per store, snapshots the address into recipient_name/recipient_phone/full_address on the group (so later address edits don't retroactively change past orders);
    • decrements Product.stock_quantity and clears the cart;
    • branches on payment_method (§5 Checkout).
  4. Route based on payment.method:
    • wallet/cash_on_delivery → go straight to an order-confirmation screen.
    • online → open payment.payment_url, then poll or listen for the order group to reach payment_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 main, delivered just marks the order delivered; no money moves yet.

6.4 Payment methods, in full (feature/payment only)

main 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). logo/cover_image aren't settable through this JSON body — that needs a multipart upload endpoint, which doesn't exist yet; flag this to backend if the store-branding screen needs it.

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. Response adds 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 main)

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 main, 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 --flush to 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 (Locations, Stores, Catalog, Cart, Checkout, Orders, Reviews, Notifications, and the Seller · * groups — Seller · Wallet and Payments only appear on feature/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 multipart/image-upload endpoint yet for store logo/cover or product images (JSON PATCH bodies can't set binary fields).
  • 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_neighborhoods from the customer-facing store serializer (needed if you want to pre-warn about delivery coverage before checkout — see §6.1).
  • The ipg online-payment callback contract (/api/v1/payments/ipg/callback/) is a best-effort integration pending confirmation against a live ipg environment — if online payments seem to hang in pending, that's the first place to check.