13 KiB
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
{
"status": "ok",
"service": "desi-clans-api",
"database": "connected"
}
Response 503 — database unreachable
{
"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[]
[
{
"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[]
[
{
"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
{
"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)
{
"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
{ "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
{
"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
{ "email": "guest@example.com", "password": "********" }
Response 200 — Token
{ "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
{
"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
{
"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
{
"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
{ "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[]
[{ "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.