Spaces:
Running
Running
| # API Reference | |
| Base URL (production): `https://shaileshrv1432-meme-backend.hf.space` | |
| Local: `http://localhost:8000` | |
| Interactive docs are served live by the running app: | |
| - **RapiDoc** β `/docs` | |
| - **ReDoc** β `/redoc` | |
| ## Conventions | |
| - **Auth**: endpoints marked π require a header `Authorization: Bearer <access_token>`. | |
| - **Admin**: endpoints marked π‘ require header `X-Admin-Secret: <ADMIN_SECRET>` and return `404` when `APP_ENV=production`. | |
| - **Body type**: most write endpoints take `application/x-www-form-urlencoded` (`Form(...)`). A few take JSON (`Body(...)`) β noted per endpoint. | |
| - **Tokens**: `access_token` (short-lived, `ACCESS_TTL_SECONDS`) + `refresh_token` (long-lived, `REFRESH_TTL_SECONDS`). Refresh via `POST /auth/refresh`. | |
| --- | |
| ## Auth β `/auth` | |
| ### `POST /auth/request-otp` | |
| Send an OTP to a phone number (creates the user if new). | |
| - Form: `phone` (E.164, e.g. `+9172048...`) | |
| - 200: `{ "message": "OTP sent" }` | |
| - 502: SMS provider send failed (invalid key / no wallet balance / KYC incomplete). With `OTP_DEBUG=true` the 502 detail contains the raw provider error. | |
| - Note: if `FAST2SMS_API_KEY` is unset, no SMS is sent β the OTP is printed to the server logs (`[DEV SMS] to=β¦ otp=β¦`) so login still works in dev. | |
| ### `GET /auth/sms-check` π‘ | |
| Diagnose OTP/SMS delivery without a real login. Admin secret via header `X-Admin-Secret` **or** `?secret=` (browser-friendly). | |
| - Query: `to` (optional 10-digit number β if given, performs a **real** test send) | |
| - 200: `{ fast2sms_key_present, key_prefix, mode, verdict, provider_response? | provider_error?, hint }` | |
| - `verdict`: `no_provider` (key unset) Β· `configured_not_tested` (no `to`) Β· `ok` Β· `send_failed` | |
| ### `POST /auth/verify-otp` | |
| Verify an OTP and receive tokens. | |
| - Form: `phone`, `otp` | |
| - 200: `{ access_token, refresh_token, user: {...} }` | |
| - 400: no user / no OTP / expired / invalid | |
| - Guest: if `GUEST_MODE_ENABLED=true` and `phone` == `GUEST_PHONE_NUMBER` with `otp` == `GUEST_OTP_CODE`, returns a guest session (`account_type: "guest"`). | |
| ### `POST /auth/set-pin` | |
| Set a login PIN for a phone user. | |
| - Form: `phone`, `pin` (4β6 digits) | |
| - 200: `{ "message": "PIN set" }` | |
| - 422: PIN not 4β6 digits | |
| ### `POST /auth/login-pin` | |
| Login with phone + PIN. | |
| - Form: `phone`, `pin` | |
| - 200: `{ access_token, refresh_token }` | |
| - 401: invalid PIN Β· 400: no user / PIN not set | |
| ### `POST /auth/refresh` | |
| Exchange a refresh token for a fresh access token. | |
| - Form: `refresh_token` | |
| - 200: `{ access_token }` Β· 401: invalid/expired | |
| ### `POST /auth/google` | |
| Verify a Google ID token and issue app tokens. | |
| - Form: `id_token` (Google ID token from the app) | |
| - 200: `{ access_token, refresh_token, user: {...} }` | |
| - 503: `GOOGLE_WEB_CLIENT_ID` not set on the server Β· 401: invalid token | |
| - Users are keyed internally by `google:<sub>`. | |
| ### `GET /auth/me` π | |
| Current user. | |
| - 200: `{ id, phone_e164, name, email, avatar_url, created_at, is_active }` | |
| ### `PATCH /auth/update-profile` π | |
| Update profile fields (form-encoded). | |
| - Form (all optional): `name`, `email`, `avatar_url` | |
| - 200: updated user object | |
| ### `DELETE /auth/delete-account` π | |
| Soft-delete (deactivate) the current account. | |
| - Form: `confirm` (bool, must be true) | |
| - 200: `{ "message": "Account deactivated" }` | |
| ### `POST /auth/debug-token` *(dev only)* | |
| Decode a JWT without verifying signature. `404` in production. | |
| --- | |
| ## Memes β `/` | |
| ### `POST /upload` π | |
| Upload an image; stores it and returns initial GIF suggestions. | |
| - Multipart: `image` (file), `mood` (optional) | |
| - 200: `{ status, uuid, object_key, url, mood, ...suggestions }` | |
| ### `POST /suggest` π | |
| Re-run suggestions for an already-uploaded image. | |
| - Form: `image_object_key`, `mood` (optional) | |
| - 200: `{ status, object_key, mood, ...suggestions }` Β· 404: key not found | |
| ### `POST /compose` π | |
| Compose a meme from form fields (single overlay). | |
| - Form: `image_object_key`, `gif_url` | `gif_object_key`, `x`, `y`, `width`, β¦ (overlay placement) | |
| ### `POST /compose/layout?output_format=gif|png|mp4` π | |
| Compose from a full JSON layout (floating images **and text** over a background). This is what the app's editor uses. | |
| - JSON body: `image_dimensions` = | |
| ```jsonc | |
| { | |
| "bgImage": { "source": "<object_key|url>", "width": 0, "height": 0, "applyCoverBlurAndContainImageFilter": false }, | |
| "floatingImages": [ { "file_url": "...", "width": 200, "relativeX": 0, "relativeY": 0, "rotation": 0, "z": 0 } ], | |
| "textOverlays": [ { "text": "LOL", "fontSize": 32, "color": "#FFFFFF", "strokeColor": "#000000", | |
| "width": 240, "height": 60, "relativeX": 0, "relativeY": 0, "rotation": 0, "z": 1 } ] | |
| } | |
| ``` | |
| - Query/body: `output_format` (default `gif`) | |
| - 200: `{ object_key, url, ... }` | |
| - **Layering**: every overlay (image or text) may carry a `z` (integer). All overlays are composited in ascending `z`, so higher `z` = on top. Omitting `z` falls back to input order (images first, then text) β fully backward compatible. | |
| - **Rotation**: `rotation` (degrees, clockwise) rotates an overlay about its centre; `relativeX/Y` stay the un-rotated box top-left. Defaults to `0`. | |
| - **Text**: rendered server-side (Pillow) as bold, stroked meme text; `color`/`strokeColor` are hex, `width` enables word-wrap, `width`+`height` define the box the text is centred in. | |
| - Tip: pass image overlay `width` only β the backend preserves the source aspect ratio (avoids stretched GIFs). | |
| ### `GET /memes?limit&offset` π | |
| List the current user's generated memes (latest first). | |
| - 200: `{ items: [{ id, object_key, content_type, created_at, presigned_url, permanent_url }], count, offset, limit }` | |
| ### `GET /memes/presign?object_key=β¦` π | |
| Get a 10-minute presigned URL for one owned meme. | |
| ### `GET /memes/{object_key}` π | |
| Stream the meme bytes through the API (permanent proxy URL, cached 1 year). | |
| --- | |
| ## Media β `/media` | |
| ### `POST /media/url` π | |
| Presign any media object (JSON body). | |
| - Body: `object_key`, `expires_seconds` (default 600) | |
| - 200: `{ object_key, url, expires_seconds }` | |
| ### `POST /media/permanent-url` π | |
| Get the permanent proxy URL for an owned media object. | |
| - Body: `object_key` | |
| - 200: `{ object_key, permanent_url, type: "proxy", expires: "never" }` | |
| --- | |
| ## Profile β `/profile` | |
| ### `GET /profile` π | |
| Current user's profile (from Postgres). | |
| ### `PUT /profile` π | |
| Update name/email/avatar (JSON body). | |
| - Body (optional): `name`, `email`, `avatar_url` | |
| - 200: `{ "message": "Profile updated" }` | |
| --- | |
| ## Subscriptions β `/subscriptions` | |
| > Requires `RAZORPAY_KEY_ID` + `RAZORPAY_KEY_SECRET` on the server. Plans (paise/INR): `weekly` 5000 / 7d, `monthly` 14000 / 30d, `yearly` 80000 / 365d. | |
| ### `POST /subscriptions/order` π | |
| Create a Razorpay order. | |
| - Form: `plan` (`weekly|monthly|yearly`) | |
| - 200: `{ order_id, amount, currency, key_id, plan }` | |
| - 400: unknown plan Β· 503: payments not configured / keys rejected (auth) Β· 502: order creation failed | |
| - With `PAYMENTS_DEBUG=true`, the 502 detail contains the raw Razorpay error (for debugging only). | |
| ### `POST /subscriptions/verify` π | |
| Verify the payment signature, activate the plan, and record a `payments` row (best-effort). | |
| - Form: `razorpay_order_id`, `razorpay_payment_id`, `razorpay_signature`, `plan` | |
| - 200: `{ status: "active", plan, expires_at }` | |
| - 400: verification failed | |
| ### `GET /subscriptions/status` π | |
| Current subscription (auto-expires past the end date). | |
| - 200: `{ plan, status, active, expires_at, is_premium, usage: { daily_meme_limit, daily_memes_used, daily_memes_remaining, premium_only_formats } }` | |
| ### `GET /subscriptions/receipt` π | |
| Most recent payment, shaped for the receipt UI. | |
| - 200: `{ id, plan, plan_label, amount, amount_display, currency, status, method, payment_id, order_id, paid_at, expires_at }` | |
| - 404: no payment on record (client falls back to plan-derived data) | |
| ### `GET /subscriptions/payments` π | |
| Full payment/transaction history, newest first. | |
| - 200: `{ payments: [ <receipt object>, β¦ ] }` (empty list if none / table absent) | |
| ### `GET /subscriptions/receipt/{payment_id}.pdf` π | |
| Download a specific payment's receipt as a PDF (generated with Pillow). Scoped to the owner. | |
| - 200: `application/pdf` (Content-Disposition attachment) | |
| - 404: receipt not found / not owned | |
| ### `GET /subscriptions/config-check` π‘ | |
| Diagnose Razorpay config without a checkout. Header `X-Admin-Secret`. | |
| - 200: `{ key_id_present, key_secret_present, key_mode, key_id_prefix, secret_length, had_whitespace_or_quotes, razorpay_lib_installed, verdict, probe_order_id?, probe_error?, hint }` | |
| - `?probe=false` skips the live βΉ1 test order. | |
| --- | |
| ## Admin β `/admin` π‘ *(404 in production)* | |
| | Method | Path | Purpose | | |
| |---|---|---| | |
| | POST | `/admin/seed-demo-user` | Create a demo user (phone, pin, otp, issue_tokens) | | |
| | POST | `/admin/minio-diagnose` | Check object-storage config | | |
| | POST | `/admin/test-sms` | Test the SMS provider | | |
| | GET | `/admin/users?limit&offset&search` | List users | | |
| | GET | `/admin/users/{id}` | Get one user | | |
| | PUT | `/admin/users/{id}` | Update name/email/is_active | | |
| | DELETE | `/admin/users/{id}` | Soft-delete (deactivate) | | |
| | GET | `/admin/cleanup-guest-data` | Purge expired guest data | | |
| --- | |
| ## Health & docs | |
| | Method | Path | Purpose | | |
| |---|---|---| | |
| | GET | `/health/deps` | Dependency/health check | | |
| | GET | `/docs` | RapiDoc UI | | |
| | GET | `/redoc` | ReDoc UI | | |