Consolidate docs and finalize APK companion updates
This commit is contained in:
@@ -1,355 +1,256 @@
|
||||
# Dewemoji API — How It Works
|
||||
# Dewemoji API — Reference and Smoke Test
|
||||
|
||||
This document explains the current API surface in the rebuild app, including filters, headers, and request/response shapes.
|
||||
This is the single API documentation source for the current Laravel rebuild.
|
||||
|
||||
## Base URLs
|
||||
|
||||
- Local: `http://127.0.0.1:8000/v1`
|
||||
- Staging: `https://dewemoji.backoffice.biz.id/v1`
|
||||
|
||||
## Auth & headers
|
||||
Convenience:
|
||||
|
||||
### License key (optional for free, required for Pro)
|
||||
You can send a license key in either header:
|
||||
```bash
|
||||
BASE=http://127.0.0.1:8000/v1
|
||||
# BASE=https://dewemoji.backoffice.biz.id/v1
|
||||
```
|
||||
|
||||
- `Authorization: Bearer YOUR_LICENSE_KEY` (recommended)
|
||||
- `X-License-Key: YOUR_LICENSE_KEY` (also supported)
|
||||
## Auth and request headers
|
||||
|
||||
### Optional headers
|
||||
- `X-Account-Id`: Optional usage association.
|
||||
- `X-Dewemoji-Frontend`: Optional frontend identifier (string).
|
||||
License-style auth (legacy-compatible):
|
||||
|
||||
- `Authorization: Bearer YOUR_LICENSE_KEY` (preferred)
|
||||
- `X-License-Key: YOUR_LICENSE_KEY` (supported)
|
||||
|
||||
Optional headers:
|
||||
|
||||
- `X-Account-Id`
|
||||
- `X-Dewemoji-Frontend`
|
||||
|
||||
Common response headers:
|
||||
|
||||
### 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):
|
||||
- `ETag`
|
||||
- `Cache-Control`
|
||||
- Rate-limit headers on free tier page 1 requests:
|
||||
- `X-RateLimit-Limit`
|
||||
- `X-RateLimit-Remaining`
|
||||
- `X-RateLimit-Reset`
|
||||
- Caching:
|
||||
- `ETag`
|
||||
- `Cache-Control`
|
||||
|
||||
## Endpoints
|
||||
## Endpoint map
|
||||
|
||||
### GET `/emojis`
|
||||
Search emojis with filters.
|
||||
Public/search:
|
||||
|
||||
- `GET /emojis`
|
||||
- `GET /emoji/{slug}` or `GET /emoji?slug=...`
|
||||
- `GET /categories`
|
||||
- `GET /health`
|
||||
|
||||
License and access lifecycle:
|
||||
|
||||
- `POST /license/verify`
|
||||
- `POST /license/activate`
|
||||
- `POST /license/deactivate`
|
||||
|
||||
Extension verification:
|
||||
|
||||
- `POST /extension/verify`
|
||||
- `GET /extension/search`
|
||||
|
||||
Metrics/internal:
|
||||
|
||||
- `GET /metrics-lite`
|
||||
- `GET /metrics`
|
||||
|
||||
Admin token endpoints (`X-Admin-Token` required):
|
||||
|
||||
- `GET /admin/settings`
|
||||
- `POST /admin/settings`
|
||||
- `GET /admin/subscriptions`
|
||||
- `POST /admin/subscription/grant`
|
||||
- `POST /admin/subscription/revoke`
|
||||
- `GET /admin/webhooks`
|
||||
- `GET /admin/webhooks/{id}`
|
||||
- `POST /admin/webhooks/{id}/replay`
|
||||
- `GET /admin/analytics`
|
||||
|
||||
## Endpoint details
|
||||
|
||||
### `GET /emojis`
|
||||
|
||||
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`.
|
||||
- `q` or `query`
|
||||
- `category`
|
||||
- `subcategory`
|
||||
- `page` (default `1`)
|
||||
- `limit` (free max `20`, pro max `50`)
|
||||
|
||||
Example:
|
||||
|
||||
```bash
|
||||
curl -s "http://127.0.0.1:8000/v1/emojis?q=love&limit=5" | jq .
|
||||
curl -s "$BASE/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.
|
||||
### `GET /emoji/{slug}` or `GET /emoji?slug=...`
|
||||
|
||||
Example:
|
||||
|
||||
```bash
|
||||
curl -s "http://127.0.0.1:8000/v1/emoji/grinning-face" | jq .
|
||||
curl -s "$BASE/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.
|
||||
- `400` `missing_slug`
|
||||
- `404` `not_found`
|
||||
|
||||
### `GET /categories`
|
||||
|
||||
Returns `category -> subcategories[]` mapping.
|
||||
|
||||
Example:
|
||||
```bash
|
||||
curl -s "http://127.0.0.1:8000/v1/categories" | jq .
|
||||
curl -s "$BASE/categories" | jq .
|
||||
```
|
||||
|
||||
Response:
|
||||
```json
|
||||
{
|
||||
"Smileys & Emotion": ["face-smiling", "face-affection", "..."],
|
||||
"People & Body": ["hand-fingers-open", "..."]
|
||||
}
|
||||
```
|
||||
### `POST /license/verify`
|
||||
|
||||
### 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 .
|
||||
KEY=YOUR_LICENSE_KEY
|
||||
curl -s -X POST "$BASE/license/verify" \
|
||||
-H "Authorization: Bearer $KEY" | jq .
|
||||
```
|
||||
|
||||
Success response:
|
||||
Success shape (example):
|
||||
|
||||
```json
|
||||
{
|
||||
"ok": true,
|
||||
"tier": "pro",
|
||||
"source": "gumroad|mayar|sandbox|...",
|
||||
"plan": "pro",
|
||||
"product_id": "…",
|
||||
"product_id": "...",
|
||||
"expires_at": null,
|
||||
"error": null
|
||||
}
|
||||
```
|
||||
|
||||
Errors:
|
||||
- `400` `missing_key`
|
||||
- `401` `invalid_license`
|
||||
### `POST /license/activate`
|
||||
|
||||
### POST `/license/activate`
|
||||
Activate a device or site session.
|
||||
|
||||
Body (JSON):
|
||||
```json
|
||||
{
|
||||
"email": "you@example.com",
|
||||
"product": "extension|site",
|
||||
"device_id": "device-123"
|
||||
}
|
||||
```bash
|
||||
KEY=YOUR_LICENSE_KEY
|
||||
curl -s -X POST "$BASE/license/activate" \
|
||||
-H "Authorization: Bearer $KEY" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"email":"you@example.com","product":"extension","device_id":"local-dev"}' | jq .
|
||||
```
|
||||
|
||||
Notes:
|
||||
- `product=site` does not require `device_id`.
|
||||
- Other products require `device_id`.
|
||||
|
||||
### POST `/license/deactivate`
|
||||
Deactivate a device.
|
||||
- `product=site` can omit `device_id`.
|
||||
|
||||
Body (JSON):
|
||||
```json
|
||||
{
|
||||
"product": "extension|site",
|
||||
"device_id": "device-123"
|
||||
}
|
||||
```
|
||||
### `POST /license/deactivate`
|
||||
|
||||
### 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 .
|
||||
KEY=YOUR_LICENSE_KEY
|
||||
curl -s -X POST "$BASE/license/deactivate" \
|
||||
-H "Authorization: Bearer $KEY" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"product":"extension","device_id":"local-dev"}' | jq .
|
||||
```
|
||||
|
||||
Errors:
|
||||
- `403 extension_unverified`
|
||||
### `GET /health`
|
||||
|
||||
## 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
|
||||
curl -s "$BASE/health" | jq .
|
||||
```
|
||||
|
||||
## Public access guard (whitelist + soft throttle)
|
||||
## Public access guard
|
||||
|
||||
Public endpoints (`/v1/emojis`, `/v1/categories`, `/v1/emoji`) are protected by a **whitelist + hourly throttle**.
|
||||
Public endpoints use whitelist + throttle behavior:
|
||||
|
||||
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`)
|
||||
- If request is whitelisted: pass.
|
||||
- If not whitelisted and `DEWEMOJI_PUBLIC_ENFORCE=true`: `403 public_access_denied`.
|
||||
- Otherwise: soft throttle with `DEWEMOJI_PUBLIC_HOURLY_LIMIT` and `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:
|
||||
|
||||
Key env vars:
|
||||
```
|
||||
```env
|
||||
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
|
||||
}
|
||||
}
|
||||
```
|
||||
Security note:
|
||||
|
||||
## Admin endpoints (token required)
|
||||
- `Origin`/`Referer` are not strong security controls. Use edge/WAF/API keys for stronger protection.
|
||||
|
||||
All admin endpoints require:
|
||||
```
|
||||
X-Admin-Token: <DEWEMOJI_ADMIN_TOKEN>
|
||||
```
|
||||
## Caching behavior
|
||||
|
||||
### Settings (feature flags / public access)
|
||||
- `GET /v1/admin/settings`
|
||||
- `POST /v1/admin/settings`
|
||||
ETag is supported. `If-None-Match` can return `304 Not Modified`.
|
||||
|
||||
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 .
|
||||
ETAG=$(curl -i "$BASE/emojis?q=love&limit=5" | awk -F': ' '/^ETag:/ {print $2}' | tr -d '\r')
|
||||
curl -i -H "If-None-Match: $ETAG" "$BASE/emojis?q=love&limit=5" | head -n 1
|
||||
```
|
||||
|
||||
### Subscriptions (admin grant/revoke)
|
||||
- `GET /v1/admin/subscriptions`
|
||||
- `POST /v1/admin/subscription/grant`
|
||||
- `POST /v1/admin/subscription/revoke`
|
||||
## Smoke test checklist
|
||||
|
||||
### Health
|
||||
|
||||
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 .
|
||||
curl -s "$BASE/health" | jq .
|
||||
```
|
||||
|
||||
### Webhooks (PayPal)
|
||||
- `POST /v1/paypal/webhook` (logs + processes)
|
||||
- `GET /v1/admin/webhooks`
|
||||
- `GET /v1/admin/webhooks/{id}`
|
||||
- `POST /v1/admin/webhooks/{id}/replay`
|
||||
Expected: `{ "ok": true, ... }`
|
||||
|
||||
Deduping:
|
||||
- PayPal `id` is stored as `event_id`. Duplicate events return `{ "ok": true, "duplicate": true }`.
|
||||
### Categories
|
||||
|
||||
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" }
|
||||
```bash
|
||||
curl -s "$BASE/categories" | jq 'keys | length'
|
||||
```
|
||||
|
||||
Other possible errors:
|
||||
- `missing_slug`
|
||||
- `missing_key`
|
||||
- `invalid_license`
|
||||
- `daily_limit_reached`
|
||||
- `data_load_failed`
|
||||
Expected: count > 0.
|
||||
|
||||
## Notes
|
||||
### Search
|
||||
|
||||
- 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.
|
||||
```bash
|
||||
curl -s "$BASE/emojis?q=love&limit=5" | jq '.items | length'
|
||||
```
|
||||
|
||||
Expected: count > 0.
|
||||
|
||||
### Detail
|
||||
|
||||
```bash
|
||||
curl -s "$BASE/emoji/grinning-face" | jq '.slug,.name'
|
||||
```
|
||||
|
||||
### Free-tier rate-limit headers
|
||||
|
||||
```bash
|
||||
curl -i "$BASE/emojis?limit=1&page=1" | grep -E "HTTP|X-RateLimit"
|
||||
```
|
||||
|
||||
### Verify / activate / deactivate
|
||||
|
||||
Use the commands in endpoint details above.
|
||||
|
||||
### Error response check
|
||||
|
||||
```bash
|
||||
curl -s "$BASE/emoji/this-does-not-exist" | jq .
|
||||
```
|
||||
|
||||
Expected `error: not_found`.
|
||||
|
||||
## Legacy compatibility notes
|
||||
|
||||
Kept for extension/backward compatibility:
|
||||
|
||||
- accepts both `q` and `query`
|
||||
- supports `Authorization` and `X-License-Key`
|
||||
- includes `X-Dewemoji-Tier`
|
||||
|
||||
Legacy contract references (`/api/*` in old stack) were consolidated here. No separate legacy API spec file is needed now.
|
||||
|
||||
Reference in New Issue
Block a user