# Shopno Books House API (v1)

Base URL: `https://api.shopnobookshouse.com/api` (frontend env `VITE_API_BASE_URL`).
All responses are JSON: success `{ "data": ... }`, error `{ "error": "message" }`, redirect `{ "redirect": "/new/path" }` with HTTP 301.
Status codes: 200, 201, 301 (moved slug), 304, 401, 403, 404, 409 (stock), 410 (deleted content), 422 (validation), 429 (rate limit), 500, 503.
Slugs are UTF-8 (Bangla allowed); send them percent-encoded in the URL.

## Shared shapes
```ts
Media       { url, width, height, alt?, caption? }
Ref         { name, slug }
BookSummary { id, title, slug, url_author_segment, author: Ref|null, price, sale_price|null, stock,
              availability: "in_stock"|"out_of_stock"|"preorder", cover: Media|null, rating_avg|null, review_count, volume_number|null }
SeoOverrides { seo_title, seo_description, canonical_override, og_image_override, noindex }
Paginated<T> { items: T[], total, page, per_page }
Faq         { q, a }
```

## Public read
| Method | Path | Notes |
|---|---|---|
| GET | `/settings` | `{ settings: {company_name, phone, whatsapp, email, address, address_bn, hours, delivery_estimate, payment_note, delivery_note, advance_note, cod_note, topbar_message, bkash_number, nagad_number, return_days, social[]}, nav: {categories: Ref[], series: Ref[]} }` |
| GET | `/home` | `{ hero, new_arrivals[], bestsellers[], offers[], categories[] (with book_count), authors[] (with book_count), featured_series[], reviews[], posts[], ranked: [{category, books[]}], faq[] }` (sections empty when no data) |
| GET | `/books?page&per_page&sort&q&category&author&series&min&max&language&binding&in_stock&on_sale` | `Paginated<BookSummary>`, 50 per page (max 50); sort: newest, price_asc, price_desc, bestselling, rating |
| GET | `/books/random?limit=8&exclude=1,2,3` | `BookSummary[]` random published books, `Cache-Control: no-store` ("You may also like") |
| GET | `/books/{author}/{slug}` | Book = BookSummary + SeoOverrides + `{series, categories[], sku, isbn, publisher, publish_year, pages, binding, paper_quality, language, description(html), gallery[], editorial_rating, reviews[], related[], bought_together[], series_volumes[], prev, next, faq[], updated_at}`. Wrong author segment → 301. |
| GET | `/categories`, `/series`, `/authors` | `(Ref & {book_count})[]` alphabetical; with `?page&per_page` (default 24) → `Paginated<Ref & {book_count}>` |
| GET | `/categories/{slug}` | Taxonomy `{name, slug, intro, description, image, book_count, faq[], children[], updated_at, ...SeoOverrides}` |
| GET | `/series/{slug}` | Taxonomy + `authors: Ref[]` |
| GET | `/series/{slug}/volumes` | `BookSummary[]` in reading order |
| GET | `/authors/{slug}` | Taxonomy + `same_as[]`, `series: Ref[]` |
| GET | `/search/autocomplete?q=` | `{ books: BookSummary[], authors: Ref[], series: Ref[] }` |
| GET | `/blog?page&category` | `Paginated<BlogPost>`, 12 per page |
| GET | `/blog/{slug}` | BlogPost `{id, title, slug, excerpt, content, cover, author_name, category, reading_minutes, published_at, updated_at, comments[], related[], ...SeoOverrides}` |
| GET | `/pages/{slug}` | `{slug, title, content, faq[], updated_at, ...SeoOverrides}` (about, terms, privacy, credits, faq) |
| GET | `/gallery` | `Media[]` |
| GET | `/coupons/public` | `[{code, description, min_total, expires_at}]` |
| GET | `/checkout/config` | `{ zones: [{district, fee}], advance_rules: [{max_total|null, advance}] }`. `delivery_zones` is seeded with all 64 districts: Dhaka ৳70, every other district ৳120 (admin-editable) |

## Public write (origin check, honeypot `website`, rate limited)
| Method | Path | Body |
|---|---|---|
| POST | `/contact` | multipart or JSON: `topic (General question|Custom print quote|Order support|Bulk or gift order), name, phone, email?, message (10-2000 chars), attachment? (PDF/JPG/PNG/WebP, max 10 MB)` |
| POST | `/print-quotes` | `{name, phone, email?, service, quantity, message}` |
| POST | `/newsletter` | `{email}` |
| POST | `/books/{id}/notify` | `{contact}` back-in-stock request |
| POST | `/books/{id}/reviews` | multipart: `name, phone?, rating 1-5, text, images[] (max 3)` → pending moderation; verified if phone has a Delivered order with this book |
| POST | `/blog/{id}/comments` | `{name, text}` → pending moderation |
| POST | `/coupons/validate` | `{code, subtotal}` → `{discount}` |
| POST | `/orders` | multipart: `name, phone, district, address, note?, payment_method (bkash|nagad), transaction_id, payment_screenshot?, coupon?, items (JSON [{book_id, qty}])` → `{order_number, total, advance}`. Prices, delivery fee and advance are recalculated server-side. |
| POST | `/orders/track` | `{order_number, phone}` → `{order_number, status, total, advance, due, delivery_code, courier, history[], items[]}` |

There are **no customer accounts**: checkout is guest-only (orders store name, phone, district and full address only), the wishlist lives on the shopper's device, and orders are followed with `/orders/track`. Staff sign in separately via `POST /admin/login` (used by the hidden, noindex `/admin/login` page).

Order statuses: Pending, Confirmed, Processing, Shipped, Delivered, Cancelled, Returned.

### Notification hooks (implement in Prompt 2/3)
- `order.created` → SMS to the shopper's phone, alert to owner
- `order.status_changed` → SMS to the shopper with status and delivery code
- `contact.created`, `quote.created` → email to owner
- `book.back_in_stock` → SMS/email to `stock_notifications`

## SEO
| Method | Path | Auth | Notes |
|---|---|---|---|
| GET | `/seo/file?name=sitemap.xml` | public | Also `sitemap-pages.xml`, `sitemap-books-{n}.xml`, `sitemap-series.xml`, `sitemap-authors.xml`, `sitemap-categories.xml`, `sitemap-blog.xml`, `llms.txt`, `rss.xml`, `{indexnow-key}.txt`. ETag/Last-Modified, disk cached. The storefront serves these at `/sitemap.xml` etc. |
| GET | `/seo/status` | admin | `{sitemap_counts, last_generated, queue_length, failed, last_pings[], indexnow_key, key_location}` |
| POST | `/seo/sitemap/regenerate` | admin | clears cache and rebuilds index |
| POST | `/seo/ping/submit-all` | manager | queues every indexable URL |
| POST | `/seo/ping/submit` | admin | `{urls: string[]}` |
| GET | `/seo/recent-changes?days=7` | admin | `[{url, updated_at}]` for Google Search Console "Request indexing" |
| POST | `/seo/ping/run` | header `X-Cron-Secret` | cron, every 1-5 min. Sends up to 10,000 deduplicated URLs to IndexNow, exponential backoff, 10-min minimum interval per URL, optional Bing URL Submission API |

Google: no ping (retired 2023) and no Indexing API (jobs/livestreams only). Submit `/sitemap.xml` once in Search Console; accurate `lastmod` does the rest.

## Admin (Bearer admin token, roles: owner, manager, editor, support)
- `POST /admin/login {email, password}` → `{token}` (12 h); `POST /admin/logout`; `GET /admin/me`
- CRUD for `books, authors, series, categories, blog_posts, pages, coupons, delivery_zones, advance_payment_rules, redirects`:
  `GET /admin/{entity}?page&per_page`, `GET /admin/{entity}/{id}`, `POST /admin/{entity}`, `PUT /admin/{entity}/{id}`, `DELETE /admin/{entity}/{id}` (optional body `{redirect_to}` for 301 instead of 410)
- Slug changes automatically create 301s (chains collapsed, loops prevented); unpublish/delete creates 410 unless `redirect_to`. `url_author_segment` is frozen at first publish.
- `POST /admin/media` (multipart `file`, optional `book_id`) → re-encoded WebP
- `GET/PUT /admin/settings` (title templates `seo_title_template_{type}`, `indexnow_enabled`, `ping_on_create|update|delete`, `bing_webmaster_api_key`, business details, `home_hero`, `home_faq`)
- `GET /admin/orders?status`, `POST /admin/orders/{id}/status {status, note, delivery_code, courier, advance_paid}`
- `POST /admin/reviews/{id}/moderate {status: approved|rejected}` (recomputes rating_avg/review_count)
