# API Specification — Desi Clans **Version:** 1.0 **Base URL:** `https://api.desiclans.com` (prod) / `http://localhost:8000` (dev) **Format:** JSON unless noted **Auth:** `Authorization: Bearer ` for protected routes --- ## 1. Conventions | Item | Rule | |------|------| | Errors | `{ "detail": "message" }` or validation array from FastAPI | | Dates | ISO 8601 UTC in JSON | | Pagination | `?limit=20&offset=0` where added later; v1 lists unbounded with sane caps in service | | IDs | Integer primary keys in v1 | | Slugs | Lowercase, hyphenated, unique per edition | --- ## 2. Platform — `/api` ### `GET /health` Liveness and PostgreSQL connectivity check. **Response 200** — database reachable ```json { "status": "ok", "service": "desi-clans-api", "database": "connected" } ``` **Response 503** — database unreachable ```json { "status": "degraded", "service": "desi-clans-api", "database": "unavailable" } ``` --- ### `GET /events` List catalog events (published by default). **Query** | Param | Type | Default | Description | |-------|------|---------|-------------| | `category` | `holi` \| `garba` \| `dandiya` \| `bhajan_clubbing` \| `special` | — | Filter by category | | `status` | `draft` \| `published` \| `cancelled` | `published` | Filter by status | | `upcoming` | boolean | `true` | If true, only `starts_at >= now` (UTC) | **Response 200** — `EventSummary[]` ```json [ { "id": 1, "slug": "rang-barse-palace-grounds-2026", "title": "Rang Barse — Palace Grounds", "category": "holi", "status": "published", "starts_at": "2026-03-09T10:00:00Z", "ends_at": "2026-03-09T18:00:00Z", "is_featured": true, "cover_image_url": "https://…", "cover_image_alt": "…", "venue": { "id": 1, "slug": "palace-grounds", "name": "Palace Grounds", "area": "Vasanth Nagar", "city": "Bengaluru", "address": "…", "map_url": "https://…", "description": "…" } } ] ``` --- ### `GET /events/{slug}` **Response 200** — `EventDetail` (includes `description`, `meta_description`, `media[]`, `ticket_types[]`) **Response 404** — unknown slug --- ### `GET /events/{id}/tickets` List purchasable ticket types for a **published** event (numeric event id). **Response 200** — `EventTicketTypePublic[]` ```json [ { "id": 1, "event_id": 1, "name": "General admission", "description": "Full-day access", "price_paise": 149900, "status": "active", "sort_order": 0, "remaining_quantity": 42 } ] ``` **Response 404** — unknown or unpublished event --- ### `GET /venues` **Response 200** — `VenuePublic[]` --- ### `GET /venues/{id}` **Response 200** — `VenuePublic` **Response 404** — unknown id --- ## 3. Public — `/api/public` (legacy, planned) Legacy public routes will mount under this prefix when business routers are enabled. --- ### `GET /config` (not yet mounted) Site-wide config: platform links, feature flags (future). **Response 200** — `SiteConfigPublic` ```json { "site_name": "Desi Clans", "city": "Bengaluru", "links": [ { "id": 1, "kind": "instagram", "label": "Instagram", "url": "https://...", "sort_order": 0 } ] } ``` --- ### `GET /editions` List published editions. **Query** | Param | Type | Description | |-------|------|-------------| | `event_type` | string | Optional enum filter | | `featured` | bool | Optional | **Response 200** — `EventEditionPublic[]` --- ### `GET /editions/{slug}` Single published edition. **Response 200** — `EventEditionPublic` **Response 404** — not found or unpublished **`EventEditionPublic` (shape)** ```json { "id": 1, "title": "Holi at Palace Grounds", "slug": "holi-palace-grounds-2026", "event_type": "holi", "city": "Bengaluru", "venue_name": "Palace Grounds", "event_date": "2026-03-10T10:00:00Z", "description": "...", "is_featured": true, "cover_image_url": "/media/...", "cover_thumb_url": "/media/.../thumb", "tickets": { "bookmyshow": "https://...", "district": null, "instagram": null }, "meta_description": "...", "media": [ { "id": 1, "url": "...", "thumb_url": "...", "caption": null, "sort_order": 0 } ] } ``` *Future:* `ticketing_mode`, `tiers[]` when native ticketing is enabled. --- ### `GET /go` Outbound redirect with click tracking. **Query** | Param | Required | Description | |-------|----------|-------------| | `kind` | yes | e.g. `bookmyshow`, `district`, `instagram` | | `edition_id` | no | Edition context | **Response 302** → destination URL --- ### `POST /subscribe` **Body** — `SubscribeRequest` ```json { "email": "user@example.com", "neighborhood": "HSR Layout" } ``` **Response 201** — created **Response 409** — email already subscribed --- ### `GET /editions/{slug}/calendar.ics` **Response 200** — `text/calendar` body --- ## 3. Share (crawler HTML) ### `GET /share/events/{slug}` Also available under public router duplicate path per deployment. **Response 200** — HTML with Open Graph tags (no auth). --- ## 4. Auth — `/api/auth` Attendee accounts (bcrypt-hashed passwords, JWT bearer tokens). Admin auth remains separate under `/api/admin` when legacy routers are mounted. | Method | Path | Auth | Description | |--------|------|------|-------------| | POST | `/register` | — | Create account | | POST | `/login` | — | Issue access token | | GET | `/me` | Bearer | Current user profile | ### `POST /register` **Body** — `RegisterRequest` ```json { "email": "guest@example.com", "password": "minimum-8-chars", "full_name": "Optional Name" } ``` **Response 201** — `Token` **Response 409** — email already registered **Response 422** — validation error (password < 8 characters, invalid email) --- ### `POST /login` **Body** — `LoginRequest` ```json { "email": "guest@example.com", "password": "********" } ``` **Response 200** — `Token` ```json { "access_token": "...", "token_type": "bearer" } ``` **Response 401** — invalid credentials JWT claims: `sub` (user id), `exp`, `type: "user"`. Lifetime from `ACCESS_TOKEN_EXPIRE_MINUTES` (server env). --- ### `GET /me` **Headers** — `Authorization: Bearer ` **Response 200** — `UserPublic` ```json { "id": 1, "email": "guest@example.com", "full_name": "Optional Name", "is_active": true, "created_at": "2026-04-04T12:00:00Z" } ``` **Response 401** — missing, invalid, or expired token Protected routes use the `CurrentUser` dependency (`get_current_user` in `app/core/deps.py`). --- ## 5. Orders & tickets — `/api/orders`, `/api/tickets` Requires attendee Bearer token. Prices are **always** computed server-side from `ticket_types.price_paise`; the client sends only `ticket_type_id` and `quantity`. Checkout reserves inventory while `status: pending`. Tickets are issued only after **verified** payment (checkout signature or webhook). Never trust client-side “payment success” flags. **Provider:** Razorpay in production (`PAYMENT_PROVIDER=razorpay`). Local dev uses `mock` (no secrets in React). Optional header: `Idempotency-Key` on `POST /orders` (unique per checkout attempt). ### `POST /orders` **Body** — `OrderCreate` ```json { "event_id": 1, "items": [{ "ticket_type_id": 2, "quantity": 2 }] } ``` **Response 201** — `OrderCheckoutResponse` (`status: pending`, `payment` block with public `key_id`, `provider_order_id`, `amount_paise`, `expires_at`) **Response 400** — invalid event, wrong event/tier pairing, empty cart **Response 401** — not authenticated **Response 409** — insufficient inventory --- ### `POST /orders/{id}/payment/verify` Server-side verification of Razorpay (or mock) checkout signature. Marks order `paid` and issues tickets. **Body** — `PaymentVerifyRequest` ```json { "provider_order_id": "order_…", "provider_payment_id": "pay_…", "signature": "…" } ``` **Response 200** — `OrderPublic` (`status: paid`) **Response 400** — invalid signature or amount mismatch **Response 409** — order not pending --- ### `POST /orders/{id}/cancel` User abandoned checkout. Releases reserved inventory. **Pending orders only.** **Response 200** — `OrderPublic` (`status: cancelled`) --- ### `POST /orders/{id}/payment/mock-complete` Development only (`ENVIRONMENT=development`, `PAYMENT_PROVIDER=mock`). Server completes mock payment without client-held secrets. --- ### Webhooks — `/api/webhooks` | Method | Path | Auth | Description | |--------|------|------|-------------| | POST | `/razorpay` | `X-Razorpay-Signature` | Razorpay events (`payment.captured`, `payment.failed`) | | POST | `/mock` | `X-Mock-Signature` | HMAC body signature (tests / local) | Webhook processing is **idempotent** (`payment_webhook_events.provider_event_id` unique). Duplicate deliveries return `{ "status": "duplicate_ignored" }`. **Env (server only):** `RAZORPAY_KEY_SECRET`, `RAZORPAY_WEBHOOK_SECRET` — never expose to the frontend. `RAZORPAY_KEY_ID` is public and returned in checkout payloads for Razorpay.js. Pending orders expire after `ORDER_PAYMENT_TIMEOUT_MINUTES` (inventory released, `status: failed`). --- ### `GET /orders/{id}` **Response 200** — `OrderPublic` (owner only) **Response 404** — not found or not your order --- ### `GET /tickets/my` **Response 200** — `TicketPublic[]` for the current user --- ### `GET /tickets/{id}` Authenticated owner only. Returns full digital pass payload including server-generated QR PNG (base64). **Response 200** — `TicketDetailPublic` (event, venue, attendee, ticket type, `code`, `qr_payload`, `qr_image_base64`, `qr_token`) **Response 404** — not found or not your ticket --- ### `POST /tickets/verify` Gate / check-in verification (server-side). Marks ticket `used` atomically — **QR reuse rejected**. **Headers:** `X-Check-In-Key` (required in production via `TICKET_VERIFY_API_KEY`) **Body** — `TicketVerifyRequest` ```json { "qr_token": "…" } ``` **Response 200** — `TicketVerifyResponse` (`valid`, `status`, `message`, `ticket_code`, `event_title`) --- **`TicketPublic.status` / `TicketDetailPublic.status`:** `valid` | `used` | `cancelled` --- ## 6. Admin — `/api/admin` Requires admin JWT. `editor` vs `super_admin` enforced per endpoint. ### `GET /me` Current admin profile. **Response 200** — `AdminUserPublic` --- ### Platform links | Method | Path | Role | Description | |--------|------|------|-------------| | GET | `/links` | editor+ | List active links | | POST | `/links` | super_admin | Create | | PATCH | `/links/{link_id}` | super_admin | Update | | DELETE | `/links/{link_id}` | super_admin | Delete | --- ### Editions | Method | Path | Role | Description | |--------|------|------|-------------| | GET | `/editions` | editor+ | All editions (incl. draft) | | POST | `/editions` | editor+ | Create | | PATCH | `/editions/{edition_id}` | editor+ | Update fields, publish flags | | POST | `/editions/{edition_id}/cover` | editor+ | Multipart cover upload | | POST | `/editions/{edition_id}/media` | editor+ | Gallery upload | | PATCH | `/media/{media_id}` | editor+ | Caption, publish flag | | POST | `/editions/{edition_id}/media/reorder` | editor+ | Body: ordered media ids | | DELETE | `/media/{media_id}` | editor+ | Remove asset | **`EventEditionAdmin`** extends public fields with `is_published`, `sort_order`, raw URLs, etc. --- ### Users & subscribers | Method | Path | Role | Description | |--------|------|------|-------------| | GET | `/users` | super_admin | List admins | | POST | `/users` | super_admin | Create admin | | GET | `/subscribers` | editor+ | Newsletter list | --- ## 7. Analytics — `/api/admin/analytics` ### `GET /clicks/summary` **Response 200** — `ClickSummaryItem[]` ```json [{ "link_kind": "bookmyshow", "count": 42 }] ``` ### `GET /clicks/recent` **Response 200** — `OutboundClickPublic[]` --- ## 8. Future — Check-in & enhancements ### Catalog — `/api/public/editions/{slug}/tiers` **Response** — active tiers with `remaining_quantity` (or sold out flag). ### Orders (extensions) | Method | Path | Auth | Description | |--------|------|------|-------------| | POST | `/` | User | Create pending order | | GET | `/` | User | My orders | | GET | `/{order_id}` | User | Order detail + tickets | | POST | `/webhooks/payment` | HMAC | Provider callback (no JWT) | ### Check-in — `/api/check-in` | Method | Path | Auth | Description | |--------|------|------|-------------| | POST | `/scan` | Staff | Body: `{ "qr": "..." }` → ticket status | --- ## 8. Enums ### `EventType` `holi` | `garba` | `dandiya` | `bhajan_clubbing` | `special` ### `PlatformKind` `bookmyshow` | `district` | `instagram` | `highape` | `youtube` | `whatsapp` | `website` | `other` ### `AdminRole` `super_admin` | `editor` ### `OrderStatus` `pending` | `paid` | `cancelled` | `failed` ### `TicketStatus` (issued ticket) `valid` | `used` | `cancelled` --- ## 9. CORS Allowed origins from `CORS_ORIGINS` env (comma-separated). Credentials enabled for admin SPA origin. --- ## 10. Static media - **Local dev:** `GET /media/{path}` - **Prod:** CDN URL built from stored key in `file_path` --- ## 11. Versioning Breaking changes require `/api/v2` prefix or negotiated migration. v1 remains unversioned path until scale demands it. --- ## 12. Related - [DATABASE.md](./DATABASE.md) - [ARCHITECTURE.md](./ARCHITECTURE.md)