# WaterTrek API (Frontend Handoff) Base URL: `/api/v1/` Auth: JWT Bearer token in `Authorization: Bearer ` ## Auth + Accounts ### Register Vendor - `POST /api/v1/accounts/register/vendor/` - Public endpoint Request: ```json { "email": "vendor@example.com", "password": "StrongPassword123", "first_name": "Nora", "last_name": "Smith", "phone_number": "+1-555-1234", "business_name": "Nora Rentals", "description": "Jet ski and boat rentals", "contact_phone": "+1-555-1234", "contact_email": "hello@norarentals.com", "address_line1": "1 Harbor Way", "city": "Miami", "state": "FL", "postal_code": "33101", "country": "USA" } ``` Response `201`: ```json { "id": 12, "email": "vendor@example.com", "first_name": "Nora", "last_name": "Smith", "phone_number": "+1-555-1234", "is_vendor": true, "is_customer": false, "vendor_profile": { "business_name": "Nora Rentals", "slug": "nora-rentals", "description": "Jet ski and boat rentals", "contact_phone": "+1-555-1234", "contact_email": "hello@norarentals.com", "address_line1": "1 Harbor Way", "address_line2": "", "city": "Miami", "state": "FL", "postal_code": "33101", "country": "USA", "created_at": "2026-04-08T17:00:00Z", "updated_at": "2026-04-08T17:00:00Z" } } ``` ### Login (JWT) - `POST /api/v1/accounts/token/` Request: ```json { "email": "vendor@example.com", "password": "StrongPassword123" } ``` Response `200`: ```json { "refresh": "", "access": "" } ``` ### Refresh JWT - `POST /api/v1/accounts/token/refresh/` Request: ```json { "refresh": "" } ``` ### Current User - `GET /api/v1/accounts/me/` - Requires JWT ### Vendor Profile Setup / Update - `GET /api/v1/accounts/vendor-profile/me/` - `PATCH /api/v1/accounts/vendor-profile/me/` - Requires vendor JWT Patch example: ```json { "business_name": "Nora Rentals LLC", "description": "Premium water equipment rentals", "city": "Fort Lauderdale" } ``` ## Equipment APIs ### Public Equipment List - `GET /api/v1/equipment/items/` - Public endpoint - Query params: - `category` (category id or slug) - `location` (icontains match) - `vendor_slug` - `min_price` - `max_price` - `available_from` (ISO datetime) - `available_to` (ISO datetime) Example: `GET /api/v1/equipment/items/?location=miami&min_price=100&max_price=300` ### Public Equipment Detail - `GET /api/v1/equipment/items/{public_id}/` - Public endpoint - **Marketing / click-through:** Each successful detail response appends: - `marketing_click_id` (integer): pass this on `POST /api/v1/booking/bookings/request/` as `marketing_click_id` so bookings can be attributed to the visit (organic or campaign). - `click_traffic_type`: `"organic"` or `"marketing"` (derived from query params below). - Optional **query params** (standard UTM + common ad IDs) are read and stored on the click row for vendor reporting: - `utm_source`, `utm_medium`, `utm_campaign`, `utm_term`, `utm_content` - `gclid`, `fbclid` - If **any** of those params is non-empty, `click_traffic_type` is `marketing`; otherwise `organic`. Example: `GET /api/v1/equipment/items/jetski-001/?utm_source=google&utm_medium=cpc&utm_campaign=spring_sale` ### Vendor Inventory CRUD - `GET /api/v1/equipment/vendor/items/` - `POST /api/v1/equipment/vendor/items/` - `GET /api/v1/equipment/vendor/items/{id}/` - `PUT /api/v1/equipment/vendor/items/{id}/` - `PATCH /api/v1/equipment/vendor/items/{id}/` - `DELETE /api/v1/equipment/vendor/items/{id}/` - Requires vendor JWT Create payload: ```json { "public_id": "jetski-001", "title": "Yamaha Jet Ski", "description": "Fast and stable for 2 riders", "details": { "engine_cc": 998, "seats": 2 }, "location": "Miami Beach", "price_per_day": "180.00", "is_active": true, "category_id": 1 } ``` ### Vendor Storefront by Slug - `GET /api/v1/equipment/storefront/{slug}/` - Public endpoint - Returns vendor profile + active inventory list ## Adventrues APIs ### Public Adventure List - `GET /api/v1/adventrues/offerings/` - Public endpoint - Query params: - `category` (category id or slug) - `location` (matches `meeting_point`) - `vendor_slug` - `min_price` - `max_price` - `available_from` (ISO datetime) - `available_to` (ISO datetime) ### Public Adventure Detail - `GET /api/v1/adventrues/offerings/{public_id}/` - Public endpoint - Same **marketing / click-through** behavior as equipment detail: response includes `marketing_click_id` and `click_traffic_type`; UTM / `gclid` / `fbclid` query params are captured when present. ### Vendor Adventure CRUD - `GET /api/v1/adventrues/vendor/offerings/` - `POST /api/v1/adventrues/vendor/offerings/` - `GET /api/v1/adventrues/vendor/offerings/{id}/` - `PUT /api/v1/adventrues/vendor/offerings/{id}/` - `PATCH /api/v1/adventrues/vendor/offerings/{id}/` - `DELETE /api/v1/adventrues/vendor/offerings/{id}/` - Requires vendor JWT Create payload: ```json { "public_id": "sunset-kayak-001", "title": "Sunset Kayak Tour", "description": "Guided paddle with sunset views", "meeting_point": "Biscayne Bay Marina", "duration_minutes": 120, "capacity": 10, "price_per_person": "75.00", "is_active": true, "category_id": 1 } ``` ### Vendor Adventure Storefront by Slug - `GET /api/v1/adventrues/storefront/{slug}/` - Public endpoint - Returns vendor profile + active adventure offerings ## Booking APIs ### Availability Check - `GET /api/v1/booking/availability/` - Public endpoint - Required query params: - exactly one of `equipment_item_id` or `adventure_offering_id` - `starts_at` (ISO datetime) - `ends_at` (ISO datetime) - Rule notes: - If availability slots exist for the target, booking requires at least one `is_available=true` slot fully covering the requested range. - Any overlapping `is_available=false` slot blocks the request. - If no slots exist yet, target is treated as bookable by default (subject to overlap checks). Example: `GET /api/v1/booking/availability/?equipment_item_id=5&starts_at=2026-04-12T10:00:00Z&ends_at=2026-04-13T10:00:00Z` Response: ```json { "equipment_item_id": 5, "adventure_offering_id": null, "starts_at": "2026-04-12T10:00:00Z", "ends_at": "2026-04-13T10:00:00Z", "is_available": true, "conflicts": 0 } ``` ### Create Booking Request (Customer) - `POST /api/v1/booking/bookings/request/` - Requires customer JWT Request: ```json { "equipment_item_id": 5, "starts_at": "2026-04-12T10:00:00Z", "ends_at": "2026-04-14T10:00:00Z", "customer_notes": "Need two life jackets", "marketing_click_id": 9042 } ``` - Optional `marketing_click_id`: integer from the latest `GET` detail response (`marketing_click_id`) for **the same** equipment item or adventure offering. Must be within **90 days** of the click. Omit for unattributed bookings (still allowed). Response includes booking with status `requested` and `listing_click` (nullable FK id) when attribution was applied. Adventure booking request example: ```json { "adventure_offering_id": 9, "starts_at": "2026-05-10T15:00:00Z", "ends_at": "2026-05-10T17:00:00Z", "participants_count": 3, "customer_notes": "First-time kayakers" } ``` ### List My Bookings - `GET /api/v1/booking/bookings/` - Requires JWT - If caller is vendor: returns bookings for their vendor profile - Else: returns bookings where caller is customer ### Booking Detail - `GET /api/v1/booking/bookings/{id}/` - Requires JWT with ownership (customer or vendor on that booking) ### Vendor Approve Booking - `POST /api/v1/booking/bookings/{id}/approve/` - Requires vendor JWT (own booking only) Payload (optional): ```json { "vendor_notes": "Approved, pickup after 9AM", "note": "Ready for pickup window" } ``` ### Vendor Decline Booking - `POST /api/v1/booking/bookings/{id}/decline/` - Requires vendor JWT (own booking only) ### Customer Cancel Booking - `POST /api/v1/booking/bookings/{id}/cancel/` - Requires customer JWT (own booking only) Payload (optional): ```json { "note": "Trip postponed to next month" } ``` ## Payment APIs (Mock Stripe) ### Create Payment Intent (Mock) - `POST /api/v1/payment/intents/` - Requires customer JWT (must own booking) - Booking must be in `approved` state Request: ```json { "booking_id": 42, "currency": "usd" } ``` Response `201`: ```json { "payment": { "id": 10, "booking_id": 42, "stripe_payment_intent_id": "pi_mock_123abc...", "stripe_charge_id": "", "amount": "360.00", "currency": "usd", "status": "requires_payment", "created_at": "2026-04-08T18:00:00Z", "updated_at": "2026-04-08T18:00:00Z" }, "client_secret": "pi_secret_mock_123abc...", "mocked": true } ``` ### Payment Status - `GET /api/v1/payment/{payment_id}/status/` - Requires JWT (booking customer or booking vendor owner) ### Stripe Webhook (Mocked) - `POST /api/v1/payment/webhooks/stripe/` - Public endpoint for local/mock integration - Idempotent by `stripe_event_id` Request: ```json { "stripe_event_id": "evt_mock_001", "event_type": "payment_intent.succeeded", "stripe_payment_intent_id": "pi_mock_123abc...", "payload": { "source": "frontend-test" } } ``` Supported `event_type` values: - `payment_intent.processing` - `payment_intent.succeeded` - `payment_intent.payment_failed` - `charge.refunded` Behavior: - Updates `PaymentRecord.status` - On `payment_intent.succeeded`, booking auto-transitions `approved -> confirmed` - Stores webhook in `WebhookEvent` and enforces idempotency ## Marketing APIs (UTM, click-through, vendor analytics) Base path: `/api/v1/marketing/` ### Track listing click (explicit) - `POST /api/v1/marketing/track/click/` - Public (optional JWT if the user is logged in; stored on the click when present) - Use when the SPA opens a listing **without** calling the public detail endpoint (e.g. client-side route from cache). Otherwise prefer relying on `GET` equipment/adventure detail, which records the click automatically. Request: ```json { "listing_type": "equipment", "public_id": "jetski-001", "utm_source": "newsletter", "utm_medium": "email", "utm_campaign": "april_promo", "utm_term": "", "utm_content": "hero", "gclid": "", "fbclid": "", "referrer": "https://mail.example.com/" } ``` - `listing_type`: `"equipment"` or `"adventure"` (must match `public_id`). Response `201`: ```json { "marketing_click_id": 9042, "traffic_type": "marketing" } ``` ### Vendor: list listing clicks - `GET /api/v1/marketing/vendor/clicks/` - Requires vendor JWT - **Required query params:** `from`, `to` — ISO-8601 datetimes (UTC), half-open range `[from, to)`. - Optional filters: `traffic_type` (`organic` | `marketing`), `listing_type` (`equipment` | `adventure`), `utm_campaign` (exact match). ### Vendor: marketing summary (conversions) - `GET /api/v1/marketing/vendor/summary/` - Requires vendor JWT - **Required query params:** `from`, `to` — same as above. Returns (shape): - `clicks.organic` / `clicks.marketing` / `clicks.total` — listing detail or track-click events in range. - `bookings_attributed.organic` — bookings in range whose `listing_click.traffic_type` is `organic`. - `bookings_attributed.marketing` — bookings in range whose `listing_click.traffic_type` is `marketing`. - `bookings_attributed.unattributed` — bookings in range with no `listing_click`. - `conversion_rate_click_to_booking.organic` — `organic` bookings ÷ `organic` clicks (omitted / `null` if clicks = 0). - `conversion_rate_click_to_booking.marketing` — same for marketing. - `campaigns` — up to 50 rows with non-empty `utm_campaign` on clicks: `utm_source`, `utm_medium`, `utm_campaign`, `clicks`, `bookings` (bookings that referenced a click with that triple), `conversion_rate`. **Interpreting organic vs marketing:** Traffic is classified **per click** from UTM / `gclid` / `fbclid`. To compare conversion rates fairly, the client should send `marketing_click_id` on checkout for both organic and paid visits when possible (so bookings are not left in `unattributed`). ## Notes for React Integration - All datetimes are ISO-8601 and UTC. - Booking overlap protection is implemented for `requested`, `approved`, and `confirmed` windows. - `requested` works as a soft hold and blocks overlapping requests until resolved. - `total_price` is computed server-side: - equipment: rounded-up day count x `price_per_day` - adventure: `participants_count` x `price_per_person` - Payment APIs are currently mocked (Stripe-style IDs and webhook event flow) to unblock frontend integration before live Stripe keys are wired. - Use `public_id` for equipment-facing detail pages and numeric `id` for vendor CRUD. - Booking domain services are implemented server-side for rules and transitions: - `is_bookable(...)` - `quote_booking(...)` - `create_booking_request(...)` - `transition_booking_status(...)` - Persist `marketing_click_id` from listing detail (or `POST .../marketing/track/click/`) and send it with `POST .../booking/bookings/request/` so vendors can tie bookings to campaigns and compare organic vs marketing conversion in `GET .../marketing/vendor/summary/`.