586 lines
13 KiB
Markdown
586 lines
13 KiB
Markdown
# 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 <access_token>` 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 <access_token>`
|
|
|
|
**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)
|