Update pricing UX, billing flows, and API rules

This commit is contained in:
Dwindi Ramadhana
2026-02-12 00:52:40 +07:00
parent cf065fab1e
commit a905256353
202 changed files with 22348 additions and 301 deletions

355
api-how-it-works.md Normal file
View File

@@ -0,0 +1,355 @@
# 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 youll see
- `X-Dewemoji-Tier`: `free` or `pro`
- `X-Dewemoji-Plan`: `free` or `pro`
- Ratelimit 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: <token>`
Response:
```json
{ "ok": true, "verified": true }
```
### GET `/extension/search`
Public search for verified extension installs only.
Headers:
- `X-Extension-Token: <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: <DEWEMOJI_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 shortlived 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.