Base URL
https://cdn.thietbihungphat.com/api/v1
Authentication
Mọi endpoint (trừ /ping và /docs) yêu cầu header X-API-Key.
X-API-Key: ckd_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Tạo key tại /api-keys (đăng nhập, vào menu "API Keys" → "Tạo key mới"). Chuỗi key chỉ hiển thị 1 lần khi tạo — hệ thống chỉ lưu SHA-256 hash, mất là phải tạo key mới.
Tuỳ chọn: gắn IP allowlist (CIDR) khi tạo key — nếu key bị lộ, allowlist là tuyến phòng thủ thứ 2. Request từ IP không khớp → 403 IP_NOT_ALLOWED.
Scopes
Mỗi key có 1 scope. Scope cao kế thừa scope thấp:
| Scope | Cho phép |
|---|---|
read | GET mọi resource public (categories, albums, tags, images) |
write | read + tạo/sửa album, upload ảnh — chỉ resource thuộc owner của key |
admin | write + delete album/ảnh, tạo category, bypass moderation queue |
Contributor tạo key chỉ thao tác được trên album mình tạo. Admin user có thể tạo admin-scope key thao tác toàn hệ thống.
Rate limit
Mỗi key có giới hạn rate_limit_per_minute (mặc định 60, configurable 1–600). Vượt → HTTP 429.
Response headers:
X-RateLimit-Limit— giới hạn cấu hìnhX-RateLimit-Remaining— còn lại trong cửa sổ 60sRetry-After— (chỉ khi 429) số giây phải đợi
Error format
Mọi lỗi trả JSON envelope thống nhất:
{
"error": {
"code": "FORBIDDEN",
"message": "Key cần scope 'write'.",
"details": { "required_scope": "write", "granted": ["read"] }
}
}
| HTTP | Code | Khi nào |
|---|---|---|
| 401 | UNAUTHENTICATED | Thiếu / sai / revoked key |
| 403 | FORBIDDEN | Scope không đủ HOẶC không phải owner |
| 403 | IP_NOT_ALLOWED | IP của request không trong allowlist của key |
| 404 | NOT_FOUND | Resource không tồn tại / chưa publish |
| 422 | VALIDATION_FAILED | Body sai format — details chứa field errors |
| 422 | ALBUM_FULL / BAD_MIME / QUOTA_EXCEEDED | Domain-level lỗi upload |
| 429 | RATE_LIMIT_EXCEEDED | details.retry_after_seconds kèm theo |
| 500 | INTERNAL_ERROR | Lỗi server (kiểm tra storage/logs/laravel.log) |
Pagination
GET list endpoints dùng Laravel paginator. Response shape:
{
"data": [ ... ],
"links": { "first": "...", "last": "...", "prev": null, "next": "..." },
"meta": { "current_page": 1, "last_page": 5, "per_page": 20, "total": 100 }
}
Endpoints
Health
GET /ping # no auth
Categories
GET /categories?parent_id=<id|root>&per_page=50 # scope: read
GET /categories/{id} # scope: read
POST /categories # scope: admin
body: {parent_id?, name, slug?, description?, meta_*?}
Albums
GET /albums?category_id=&tag=&sku=&model=&q=&per_page=20 # scope: read
GET /albums/{id} # scope: read — bao gồm images đầy đủ
POST /albums # scope: write
body: {category_id, name, sku?, model?, slug?, description?,
visibility?, tags?, meta_*?}
PATCH /albums/{id} # scope: write (owner) hoặc admin
DELETE /albums/{id} # scope: admin (owner)
Images + Variants
POST /albums/{id}/images # scope: write (owner)
multipart: file=<binary>, alt_text=<string>
trả 201, original_url + variants {avif, webp, thumb_*}
DELETE /images/{id} # scope: admin (owner)
GET /images/{id}/variants # scope: read
trả list các variant đã generate + pending_variants
GET /images/{id}/variants/{key} # scope: read
302 redirect tới URL variant; 202 nếu enabled nhưng chưa gen;
404 UNKNOWN_VARIANT / VARIANT_DISABLED
Variant keys mặc định: webp, avif (full-size, luôn có), thumb_sm (320w), thumb_md (640w), thumb_lg (1280w). Admin có thể bật thêm: social_og (1200×630), social_twitter, card_square (800×800), card_landscape, instagram_post, instagram_story, hero_1920, mobile_thumb — hoặc tạo variant custom tại /admin/settings → Hình ảnh.
Tags
GET /tags?q=&per_page=50 # scope: read
Examples
Tạo album + upload ảnh
KEY="ckd_xxxx..."
BASE="https://cdn.thietbihungphat.com/api/v1"
# 1. Tạo album trong category id=2 (Đèn LED)
ALBUM=$(curl -sS -H "X-API-Key: $KEY" -H "Accept: application/json" \
-H "Content-Type: application/json" \
-d '{"category_id":2,"name":"Đèn pha 200W","sku":"LED-200W-PHA","model":"Panasonic LP-200"}' \
$BASE/albums | jq -r .data.id)
# 2. Upload ảnh
curl -sS -H "X-API-Key: $KEY" \
-F "file=@/path/to/photo.jpg" \
-F "alt_text=Đèn pha 200W góc nghiêng" \
$BASE/albums/$ALBUM/images
# 3. Đọc lại album
curl -sS -H "X-API-Key: $KEY" $BASE/albums/$ALBUM | jq .
Tìm album theo SKU
# Exact match
curl -sS -H "X-API-Key: $KEY" "$BASE/albums?sku=LED-200W-PHA"
# Multi-field search (name + sku + model)
curl -sS -H "X-API-Key: $KEY" "$BASE/albums?q=Panasonic"
# By tag + category
curl -sS -H "X-API-Key: $KEY" "$BASE/albums?category_id=2&tag=led&per_page=10"
Reading public albums (no key needed via website)
Public website pages cũng phục vụ ảnh trực tiếp — nếu chỉ cần đọc, nhúng <img src> URL từ /storage/images/... đã có cache header immutable 1 năm. API chỉ cần khi cần JSON metadata hoặc thao tác ghi.
Webhooks
Đăng ký webhook tại /webhooks để nhận event khi:
album.published— admin/mod duyệt albumimage.processed— job Imagick xong, AVIF/WebP variants readycomment.created— comment mới được post
Mỗi delivery có header X-Webhook-Signature: sha256=<hmac_sha256(body, secret)>. Server bạn verify chữ ký rồi trả 2xx trong 10 giây. Không 2xx → retry với backoff 30s / 5m / 30m. Sau 3 lần fail → status=abandoned.
Limits
| Setting | Mặc định | Mô tả |
|---|---|---|
upload_max_mb | 10 | Kích thước file ảnh tối đa |
album_max_images | 200 | Ảnh tối đa / album |
default_quota_mb | 500 | Dung lượng tối đa / user |
allowed_mimes | jpeg, png, webp, avif, gif | MIME được phép (validate bằng magic bytes) |
Admin có thể điều chỉnh tại /admin/settings.
Versioning
Path prefix /api/v1/. Phá vỡ tương thích = tăng version (vd /v2/). Trong v1:
- Field mới có thể được thêm — clients không nên reject extra fields
- Field hiện tại sẽ không bị đổi nghĩa / xoá trong vòng đời v1
- Status code không đổi
Cần hỗ trợ?
Phát hiện bug hoặc cần endpoint mới? Liên hệ qua email admin trong /admin/settings (operator). Hoặc thử endpoint trực tiếp trong Swagger UI để debug.