# Dewemoji API — How It Works This document explains the current API surface in the rebuild app, including filters, headers, and request/response shapes. ## Base URLs - Local: `http://127.0.0.1:8000/v1` - Staging: `https://dewemoji.backoffice.biz.id/v1` ## Auth & headers ### License key (optional for free, required for Pro) You can send a license key in either header: - `Authorization: Bearer YOUR_LICENSE_KEY` (recommended) - `X-License-Key: YOUR_LICENSE_KEY` (also supported) ### Optional headers - `X-Account-Id`: Optional usage association. - `X-Dewemoji-Frontend`: Optional frontend identifier (string). ### Response headers you’ll see - `X-Dewemoji-Tier`: `free` or `pro` - `X-Dewemoji-Plan`: `free` or `pro` - Rate‑limit headers on free tier (page=1 only): - `X-RateLimit-Limit` - `X-RateLimit-Remaining` - `X-RateLimit-Reset` - Caching: - `ETag` - `Cache-Control` ## Endpoints ### GET `/emojis` Search emojis with filters. Query params: - `q` (string): search query (also accepts `query`) - `category` (string): category name (e.g. `Smileys & Emotion`) - `subcategory` (string): subcategory name (will be slugified internally) - `page` (int, default 1) - `limit` (int): - Free tier: max 20 - Pro tier: max 50 Behavior: - If `q` is empty, it returns all emojis (subject to pagination). - If `q` has multiple terms, all terms must match. - `category` must match the exact category label in the dataset. - `subcategory` is matched by slugifying both the request and dataset. - `plan` field in response will be `free` or `pro`. Example: ```bash curl -s "http://127.0.0.1:8000/v1/emojis?q=love&limit=5" | jq . ``` Response (shape): ```json { "items": [ { "emoji": "😍", "name": "smiling face with heart-eyes", "slug": "smiling-face-with-heart-eyes", "category": "Smileys & Emotion", "subcategory": "face-affection", "supports_skin_tone": false, "summary": "...", "unified": "U+1F60D", "codepoints": ["1F60D"], "shortcodes": [":smiling-face-with-heart-eyes:", "..."], "aliases": [], "keywords_en": ["love", "heart", "..."], "keywords_id": ["cinta", "..."], "related": ["🥰", "🤩", "😘"], "intent_tags": ["love", "affection"] } ], "total": 2131, "page": 1, "limit": 20, "plan": "free" } ``` ### GET `/emoji/{slug}` or `/emoji?slug=...` Fetch detail for a specific emoji. Example: ```bash curl -s "http://127.0.0.1:8000/v1/emoji/grinning-face" | jq . ``` Errors: - `400` if slug is missing (`error: missing_slug`) - `404` if slug is not found (`error: not_found`) ### GET `/categories` Returns a map of category → list of subcategories. Example: ```bash curl -s "http://127.0.0.1:8000/v1/categories" | jq . ``` Response: ```json { "Smileys & Emotion": ["face-smiling", "face-affection", "..."], "People & Body": ["hand-fingers-open", "..."] } ``` ### POST `/license/verify` Validate a license key. Headers: - `Authorization: Bearer YOUR_LICENSE_KEY` (or `X-License-Key`) Example: ```bash curl -s -X POST "http://127.0.0.1:8000/v1/license/verify" \ -H "Authorization: Bearer YOUR_LICENSE_KEY" | jq . ``` Success response: ```json { "ok": true, "tier": "pro", "source": "gumroad|mayar|sandbox|...", "plan": "pro", "product_id": "…", "expires_at": null, "error": null } ``` Errors: - `400` `missing_key` - `401` `invalid_license` ### POST `/license/activate` Activate a device or site session. Body (JSON): ```json { "email": "you@example.com", "product": "extension|site", "device_id": "device-123" } ``` Notes: - `product=site` does not require `device_id`. - Other products require `device_id`. ### POST `/license/deactivate` Deactivate a device. Body (JSON): ```json { "product": "extension|site", "device_id": "device-123" } ``` ### GET `/health` Basic health check. Response: ```json { "ok": true, "time": "...", "app": "Dewemoji" } ``` ### GET `/metrics-lite` Lightweight metrics (if enabled). ### GET `/metrics` Full metrics (requires token or allowed IP). ## Extension verification (public, no login) These endpoints allow **verified extension installs** to access public search without login. Fallback (temporary): - If Verified Access token is not available, the server can accept `X-Extension-Id` as a **soft allowlist** signal. This is **spoofable** and should not be treated as a strong security boundary. ### POST `/extension/verify` Verifies `X-Extension-Token` by calling Google Instance ID API. Headers: - `X-Extension-Token: ` Response: ```json { "ok": true, "verified": true } ``` ### GET `/extension/search` Public search for verified extension installs only. Headers: - `X-Extension-Token: ` Example: ```bash curl -s "http://127.0.0.1:8000/v1/extension/search?q=snail" \ -H "X-Extension-Token: $EXT_TOKEN" | jq . ``` Errors: - `403 extension_unverified` ## Caching The API uses `ETag` and returns `304 Not Modified` if the client sends: ``` If-None-Match: "etag-value" ``` Example: ```bash ETAG=$(curl -i "http://127.0.0.1:8000/v1/emojis?q=love&limit=5" | awk -F': ' '/^ETag:/ {print $2}' | tr -d '\r') curl -i -H "If-None-Match: $ETAG" "http://127.0.0.1:8000/v1/emojis?q=love&limit=5" | head -n 1 ``` ## Public access guard (whitelist + soft throttle) Public endpoints (`/v1/emojis`, `/v1/categories`, `/v1/emoji`) are protected by a **whitelist + hourly throttle**. Behavior: - If the request is **whitelisted**, it passes without throttling. - If not whitelisted: - If `DEWEMOJI_PUBLIC_ENFORCE=true` → `403 public_access_denied` - Else → **soft throttle** with `DEWEMOJI_PUBLIC_HOURLY_LIMIT` (HTTP `429 public_rate_limited`) Whitelist rules: - `Origin` is in `DEWEMOJI_PUBLIC_ORIGINS` - Or `X-Dewemoji-Frontend` / `User-Agent` contains a configured extension ID Key env vars: ``` DEWEMOJI_PUBLIC_ENFORCE=true|false DEWEMOJI_PUBLIC_ORIGINS=https://dewemoji.com,https://www.dewemoji.com DEWEMOJI_PUBLIC_HOURLY_LIMIT=5000 DEWEMOJI_EXTENSION_IDS=chrome-extension://... ``` Rate limit response example: ```json { "ok": false, "error": "public_rate_limited", "usage": { "used": 3, "limit": 3, "remaining": 0, "window": "hourly", "window_ends_at": "2026-02-08T14:00:00+00:00", "window_ends_at_unix": 1770559200 } } ``` ## Admin endpoints (token required) All admin endpoints require: ``` X-Admin-Token: ``` ### Settings (feature flags / public access) - `GET /v1/admin/settings` - `POST /v1/admin/settings` Example: ```bash curl -s -X POST "http://127.0.0.1:8000/v1/admin/settings" \ -H "X-Admin-Token: $DEWEMOJI_ADMIN_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "settings": { "maintenance_enabled": false, "public_enforce": true, "public_hourly_limit": 5000, "public_origins": ["https://dewemoji.com","https://www.dewemoji.com"], "public_extension_ids": ["chrome-extension://yourid"] } }' | jq . ``` ### Subscriptions (admin grant/revoke) - `GET /v1/admin/subscriptions` - `POST /v1/admin/subscription/grant` - `POST /v1/admin/subscription/revoke` Grant example: ```bash curl -s -X POST "http://127.0.0.1:8000/v1/admin/subscription/grant" \ -H "X-Admin-Token: $DEWEMOJI_ADMIN_TOKEN" \ -H "Content-Type: application/json" \ -d '{"email":"test@example.com","plan":"personal","status":"active","provider":"admin"}' | jq . ``` ### Webhooks (PayPal) - `POST /v1/paypal/webhook` (logs + processes) - `GET /v1/admin/webhooks` - `GET /v1/admin/webhooks/{id}` - `POST /v1/admin/webhooks/{id}/replay` Deduping: - PayPal `id` is stored as `event_id`. Duplicate events return `{ "ok": true, "duplicate": true }`. Note: - PayPal **signature verification is not implemented yet** (TODO before production). ### Analytics - `GET /v1/admin/analytics` (counts for users/keywords/subscriptions/webhooks) ## Security note: Origin/Referer are spoofable The `Origin` / `Referer` headers are **not** a strong security boundary. They are used to reduce casual abuse only. Options to harden public access: - **Require API keys** for unlimited access (personal users). - **Rate limit at the edge** (Cloudflare / Nginx). - **Signed requests** for extension (HMAC or short‑lived tokens). - **Shared secret header** for internal services. - **WAF rules** for `/v1/*` endpoints. ## Error payloads (common) ```json { "ok": false, "error": "not_found" } ``` Other possible errors: - `missing_slug` - `missing_key` - `invalid_license` - `daily_limit_reached` - `data_load_failed` ## Notes - Current dataset contains **EN + ID** keywords. - API reads from `app/data/emojis.json` (cache-first strategy). - Free tier limit is enforced on **page=1** requests.