Bỏ qua, tới nội dung chính

Tài liệu

Tài liệu API

Tạo giọng nói, ảnh và video bằng một lời gọi HTTP. Mọi ví dụ dưới đây chạy thật — bạn chỉ cần thay API key của mình vào là dùng được ngay.

Base URLhttps://api.shopapi.vnNhấn Ctrl + K để tìm nhanh trong tài liệu.
Mục lục

Bắt đầu

Bắt đầu nhanh

Ba bước, khoảng hai phút, là bạn có file audio đầu tiên.

  1. Lấy API key. Bạn đăng ký tài khoản rồi vào Bảng điều khiển → API key để tạo key mới. Key bắt đầu bằng sk_live_chỉ hiện đúng một lần — bạn chép ngay và cất vào biến môi trường.
  2. Gọi lời gọi đầu tiên. Chép đoạn code bên cạnh, thay key của bạn vào rồi chạy. Bạn nhận về ngay một job_id kèm mã 202, không phải chờ.
  3. Lấy kết quả. Hỏi trạng thái bằng GET /v1/jobs/{id}, hoặc nghe luồng realtime để biết tiến độ ngay khi có thay đổi. Khi statussucceeded, link tải nằm ở output.url.

Bạn chỉ trả tiền cho phần thật sự dùng

Lúc tạo job, hệ thống tạm giữ tiền theo ước tính. Khi job xong mới trừ đúng theo lượng tài nguyên thực tế và trả lại phần thừa. Job hỏng thì bạn được hoàn đủ, không mất đồng nào.

Ví dụ lời gọi đầu tiên

POST /v1/tts
curl -X POST https://api.shopapi.vn/v1/tts \
  -H "Authorization: Bearer sk_live_XXXXXXXXXXXX" \
  -d '{"text":"Xin chào Việt Nam"}'
Phản hồi 202
{
  "id": "job_x7k2m9p4qr8s",
  "object": "job",
  "type": "tts",
  "status": "queued",
  "estimated_cost": "200000000",
  "estimated_seconds": 12,
  "queue_position": 1,
  "created_at": "2026-08-03T10:30:00Z"
}

Xác thực

Mọi lời gọi đều mang API key trong header Authorization.

Bạn gửi kèm header Authorization: Bearer sk_live_... trong mỗi yêu cầu. Không có key, hoặc key sai, bạn nhận về lỗi 401 invalid_api_key.

Key chỉ hiện đúng một lần lúc tạo. Chúng tôi không lưu key nguyên văn — trong cơ sở dữ liệu chỉ có bản băm cùng 8 ký tự đầu để bạn nhận ra key nào là key nào. Nếu bạn làm mất key thì không ai xem lại được, kể cả chúng tôi; bạn thu hồi key cũ và tạo key mới.

Key là mật khẩu tài khoản của bạn: đừng đặt vào code chạy trên trình duyệt hay ứng dụng di động, cũng đừng đẩy lên kho code công khai. Hãy để key ở phía máy chủ, đọc từ biến môi trường.

Trình duyệt và code dùng token khác nhau

Giao diện web đăng nhập bằng phiên riêng, còn code của bạn dùng API key sk_live_. Hai loại token không dùng lẫn cho nhau được.

Ví dụ header xác thực

Gửi kèm API key
curl https://api.shopapi.vn/v1/balance \
  -H "Authorization: Bearer sk_live_XXXXXXXXXXXX"
Khi key sai — 401
{
  "error": {
    "code": "invalid_api_key",
    "message": "Key này sai hoặc đã bị thu hồi. Bạn tạo key mới trong bảng điều khiển rồi thay vào code.",
    "type": "authentication_error",
    "param": null,
    "request_id": "req_4d7f2a9c1b0e"
  }
}

Đơn vị tiền: micro-VND

Mọi số tiền trong API là micro-VND và luôn được truyền dưới dạng chuỗi.

1₫ = 1.000.000 µVND. Ví dụ "957000000" nghĩa là 957 đồng. Muốn ra số đồng, bạn chia cho 1.000.000.

Sao lại rắc rối vậy? Vì giá một giây audio nhỏ hơn một đồng rất nhiều. Nếu làm tròn đến đồng ở từng job, sai số cộng dồn qua hàng triệu job sẽ thành con số lớn. Dùng đơn vị nhỏ hơn một triệu lần thì mọi phép cộng trừ đều là số nguyên, không bao giờ lệch.

Và vì sao là chuỗi chứ không phải số? Vì JavaScript không biểu diễn chính xác số nguyên lớn: số dư 2.500.000₫ là 2.500.000.000.000 µVND, vượt xa vùng an toàn của kiểu số thông thường. Bạn hãy đọc nó bằng kiểu số nguyên lớn (BigInt, int, decimal) rồi mới tính.

Giá trị trong JSONBạn hiểu làGhi chú
"1000000"1₫Đơn vị nhỏ nhất mà bạn thường gặp: đúng một đồng
"200000000"200₫Giá một phút giọng đọc
"957000000"957₫Tiền thực trừ của job trong ví dụ ở mục Xem một job
"2500000000000"2.500.000₫Số dư ví trong ví dụ ở mục Số dư

Đừng dùng số thực để tính tiền

parseFloat hay float sẽ làm tròn sai ở những con số lớn. Bạn giữ nguyên chuỗi, đổi sang số nguyên lớn khi cần cộng trừ, và chỉ chia 1.000.000 ở bước cuối cùng khi hiển thị cho người xem.

Ví dụ quy đổi micro-VND

Trong phản hồi job
{
  "cost": "957000000",      // 957₫ đã trừ
  "refunded": "43000000"    //  43₫ trả lại phần thừa
}
Quy đổi
// Đọc số tiền: chia cho 1.000.000
const vnd = Number(BigInt(job.cost) / 1000000n); // 957

// Gửi số tiền: nhân lên rồi đổi sang chuỗi
const amount = (500000n * 1000000n).toString();  // "500000000000"

ID có tiền tố

Nhìn tiền tố là biết ngay ID đó thuộc loại nào — tiện khi đọc log lúc nửa đêm.

Mọi ID đều là chuỗi ngẫu nhiên có tiền tố theo loại đối tượng, ví dụ job_x7k2m9p4qr8s. ID không mang thứ tự và không đoán được, nên bạn cứ lưu nguyên chuỗi, đừng cắt tiền tố ra.

Tiền tốĐối tượngVí dụ
usr_Người dùngusr_7k2m9p4qr8sd
job_Job — mỗi lần bạn gọi API tạo ra một jobjob_x7k2m9p4qr8s
key_API keykey_3n8v2c5xq1wz
led_Bút toán sổ cái — mỗi dòng thay đổi số dưled_a1b2c3d4e5f6
wkr_Worker — máy đang chạy jobwkr_voice_vm01
acc_Account trong kho nội bộacc_5t6y7u8i9o0p
lea_Phiếu mượn accountlea_2q3w4e5r6t7y
txn_Giao dịch nạp tiềntxn_p9q8r7s6t5u4

Chống gửi trùng (Idempotency)

Gửi lại cùng một yêu cầu bao nhiêu lần cũng chỉ tạo đúng một job — và chỉ bị trừ tiền một lần.

Mạng ở Việt Nam đôi khi rớt giữa chừng: yêu cầu của bạn đã tới nơi nhưng phản hồi không về được. Lúc đó bạn không biết job đã tạo hay chưa. Nếu gửi lại mà không có gì bảo vệ, bạn sẽ tạo hai job và trả tiền hai lần.

Vì vậy mọi endpoint tạo job đều nhận header Idempotency-Key. Bạn tự sinh một chuỗi duy nhất cho mỗi ý định tạo job (khuyến nghị dùng uuid) và gửi kèm. Nếu chúng tôi đã xử lý key đó rồi, lần gọi sau nhận lại đúng job cũ thay vì tạo job mới.

Dùng lại key cũ nhưng đổi nội dung body thì bạn nhận lỗi 409 idempotency_conflict — đây là hàng rào an toàn, tránh việc một key vô tình đại diện cho hai job khác nhau.

Mẹo thực tế

Sinh key ngay khi người dùng bấm nút, rồi giữ nguyên key đó cho mọi lần thử lại của cùng thao tác. Đừng sinh key mới trong vòng lặp retry — như vậy là mất tác dụng.

Ví dụ header chống gửi trùng

Idempotency-Key
curl -X POST https://api.shopapi.vn/v1/tts \
  -H "Authorization: Bearer sk_live_XXXXXXXXXXXX" \
  -H "Idempotency-Key: 9d1f2c84-4b7a-4f0e-9a2d-6c5b3e8a1f47" \
  -H "Content-Type: application/json" \
  -d '{"text":"Xin chào Việt Nam"}'

Giới hạn tần suất

Mỗi phản hồi đều kèm ba header cho biết bạn còn bao nhiêu lượt gọi.

Bạn đọc X-RateLimit-Remaining để biết còn bao nhiêu lượt trong cửa sổ hiện tại, và X-RateLimit-Reset (dấu thời gian Unix) để biết khi nào bộ đếm làm mới. Vượt giới hạn thì bạn nhận 429 rate_limit_exceeded kèm header Retry-After — chờ đúng số giây đó rồi gọi lại là được.

Giới hạn tính theo hạng tài khoản. Ba con số quan trọng: số yêu cầu mỗi phút, số job được chạy cùng lúc, và số job mỗi ngày.

HạngYêu cầu / phútJob chạy cùng lúcJob / ngày
freeMiễn phí10120
starterKhởi động6051.000
proChuyên nghiệp3002010.000
businessDoanh nghiệp1.000100Không giới hạn

Gặp 429 thì làm gì

Bạn chờ theo Retry-After rồi giãn dần khoảng cách giữa các lần thử (backoff) thay vì gọi dồn dập. Cần chạy nhiều hơn nữa thì nâng hạng ở trang Bảng giá.

Ví dụ header giới hạn tần suất

Trong mọi phản hồi
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 57
X-RateLimit-Reset: 1785312060

Tham chiếu API

Tạo job

Ba endpoint, cùng một khuôn mẫu. Gửi xong bạn nhận ngay `job_id` và mã 202 — không phải chờ. Tiền được tạm giữ lúc này, quyết toán khi job xong.

Tạo giọng nói

Biến văn bản tiếng Việt thành file audio.

POST/v1/tts

Bạn gửi văn bản, hệ thống trả về ngay một job đang xếp hàng. Tiền được TẠM GIỮ theo ước tính, khi job xong mới trừ đúng theo số giây audio thực tế và trả lại phần thừa. Job hỏng thì bạn không mất đồng nào.

Tham số trong body

Tham sốKiểuMô tả
textBắt buộcstring1..100.000 ký tựNội dung cần đọc.
voice_idTuỳ chọnstringMặc định: vi_female_01Giọng đọc. Xem danh sách giọng ở mục Giọng nói.
speedTuỳ chọnnumberMặc định: 1.00.5..2.0Tốc độ đọc. 1.0 là bình thường.
formatTuỳ chọnstringMặc định: mp3mp3wavĐịnh dạng file audio trả về.
webhook_urlTuỳ chọnstringĐường dẫn để hệ thống báo về khi job xong, thay vì bạn phải hỏi liên tục.

Ví dụ code cho Tạo giọng nói

POST /v1/tts
curl -X POST https://api.shopapi.vn/v1/tts \
  -H "Authorization: Bearer sk_live_XXXXXXXXXXXX" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
  "text": "Xin chào, đây là giọng đọc tiếng Việt từ ShopAPI.",
  "voice_id": "vi_female_01",
  "speed": 1,
  "format": "mp3"
}'
Phản hồi202
{
  "id": "job_x7k2m9p4qr8s",
  "object": "job",
  "type": "tts",
  "status": "queued",
  "estimated_cost": "200000000",
  "estimated_seconds": 12,
  "queue_position": 1,
  "created_at": "2026-08-03T10:30:00Z"
}

Tạo ảnh

Sinh ảnh từ mô tả bằng chữ.

POST/v1/images/generations

Mỗi ảnh được tính tiền riêng, nên `n: 4` sẽ tạm giữ gấp bốn lần. Chỉ những ảnh ra thành công mới bị tính tiền.

Tham số trong body

Tham sốKiểuMô tả
promptBắt buộcstringMô tả ảnh bạn muốn. Tiếng Việt hoặc tiếng Anh đều được.
nTuỳ chọnintegerMặc định: 11..8Số ảnh cần tạo. Mỗi ảnh tính tiền riêng.
aspect_ratioTuỳ chọnstringMặc định: 16:916:99:161:14:33:4Tỉ lệ khung ảnh.
seedTuỳ chọnintegerCùng seed và cùng prompt sẽ cho ra ảnh gần giống nhau — tiện khi bạn muốn lặp lại kết quả.
reference_imagesTuỳ chọnstring[]tối đa 3 ảnhẢnh tham chiếu về phong cách hoặc bố cục.

Ví dụ code cho Tạo ảnh

POST /v1/images/generations
curl -X POST https://api.shopapi.vn/v1/images/generations \
  -H "Authorization: Bearer sk_live_XXXXXXXXXXXX" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
  "prompt": "một con mèo phi hành gia, phong cách điện ảnh",
  "n": 1,
  "aspect_ratio": "16:9"
}'
Phản hồi202
{
  "id": "job_b3n8v2c5xq1w",
  "object": "job",
  "type": "image",
  "status": "queued",
  "estimated_cost": "100000000",
  "estimated_seconds": 25,
  "queue_position": 2,
  "created_at": "2026-08-03T10:30:00Z"
}

Tạo video

Sinh video ngắn từ mô tả, hoặc từ một ảnh có sẵn.

POST/v1/videos/generations

Nên để `engine: "auto"` — hệ thống tự chọn máy rảnh nhất, và khi một engine gặp sự cố thì job vẫn chạy được ở engine kia. Có `image_url` thì job chuyển sang chế độ ảnh-thành-video.

Tham số trong body

Tham sốKiểuMô tả
promptBắt buộcstringMô tả cảnh quay bạn muốn.
engineTuỳ chọnstringMặc định: autoautoveo3seedanceMáy xử lý. Để "auto" cho hệ thống tự chọn.
durationTuỳ chọnintegerMặc định: 8Độ dài video, tính bằng giây. Veo3 chỉ nhận 8; Seedance nhận 5 hoặc 10.
aspect_ratioTuỳ chọnstringMặc định: 16:916:99:161:14:33:4Tỉ lệ khung hình.
image_urlTuỳ chọnstringẢnh khởi đầu. Có ảnh thì video sẽ chuyển động từ chính ảnh đó.
webhook_urlTuỳ chọnstringĐường dẫn nhận thông báo khi job xong.

Ví dụ code cho Tạo video

POST /v1/videos/generations
curl -X POST https://api.shopapi.vn/v1/videos/generations \
  -H "Authorization: Bearer sk_live_XXXXXXXXXXXX" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
  "prompt": "máy quay lướt qua thành phố lúc hoàng hôn",
  "engine": "auto",
  "duration": 8,
  "aspect_ratio": "16:9"
}'
Phản hồi202
{
  "id": "job_k9d4f7g2hj3l",
  "object": "job",
  "type": "video",
  "status": "queued",
  "estimated_cost": "500000000",
  "estimated_seconds": 180,
  "queue_position": 3,
  "created_at": "2026-08-03T10:30:00Z"
}

Tham chiếu API

Theo dõi job

Hỏi trạng thái, nghe luồng realtime, hoặc huỷ khi cần.

Xem một job

Lấy trạng thái và kết quả của một job.

GET/v1/jobs/{id}
Cần API key

Khi `status` là `succeeded`, trường `output.url` chứa link tải kết quả, có hạn 7 ngày. Bạn nên tải file về lưu ở nơi của mình thay vì dùng trực tiếp link này lâu dài.

Ví dụ code cho Xem một job

GET /v1/jobs/{id}
curl -X GET https://api.shopapi.vn/v1/jobs/<id> \
  -H "Authorization: Bearer sk_live_XXXXXXXXXXXX"
Phản hồi200
{
  "id": "job_x7k2m9p4qr8s",
  "status": "succeeded",
  "progress": 100,
  "output": {
    "url": "https://cdn.shopapi.vn/o/2026/08/03/x7k2m9.mp3",
    "expires_at": "2026-08-10T10:30:00Z",
    "size_bytes": 2847293,
    "duration_seconds": 287.4,
    "format": "mp3"
  },
  "usage": {
    "characters": 4482,
    "audio_seconds": 287.4
  },
  "cost": "957000000",
  "refunded": "43000000",
  "created_at": "2026-08-03T10:30:00Z",
  "completed_at": "2026-08-03T10:31:12Z"
}

Liệt kê job

Danh sách job của bạn, có lọc và phân trang.

GET/v1/jobs
Cần API key

Tham số trên query string

Tham sốKiểuMô tả
statusTuỳ chọnstringqueuedrunningretryingsucceededfailedcancelledrejectedLọc theo trạng thái.
typeTuỳ chọnstringttsimagevideoLọc theo loại dịch vụ.
limitTuỳ chọnintegerMặc định: 201..100Số job mỗi trang.
cursorTuỳ chọnstringCon trỏ trang tiếp theo, lấy từ `next_cursor` của lần gọi trước.

Ví dụ code cho Liệt kê job

GET /v1/jobs
curl -X GET https://api.shopapi.vn/v1/jobs \
  -H "Authorization: Bearer sk_live_XXXXXXXXXXXX"
Phản hồi200
{
  "object": "list",
  "data": [
    {
      "id": "job_x7k2m9p4qr8s",
      "object": "job",
      "type": "tts",
      "status": "succeeded",
      "progress": 100
    }
  ],
  "has_more": true,
  "next_cursor": "job_x7k2m9p4qr8s"
}

Huỷ job

Dừng một job đang xếp hàng hoặc đang chạy.

POST/v1/jobs/{id}/cancel
Cần API key

Toàn bộ tiền tạm giữ được trả lại ví ngay lập tức.

Ví dụ code cho Huỷ job

POST /v1/jobs/{id}/cancel
curl -X POST https://api.shopapi.vn/v1/jobs/<id>/cancel \
  -H "Authorization: Bearer sk_live_XXXXXXXXXXXX"
Phản hồi200
{
  "id": "job_x7k2m9p4qr8s",
  "status": "cancelled",
  "cost": "0",
  "refunded": "200000000"
}

Theo dõi tiến độ realtime

Luồng SSE đẩy tiến độ về ngay khi có thay đổi.

GET/v1/jobs/{id}/events
Cần API key

Đây là cách nhẹ nhất để theo dõi job: bạn mở một kết nối, hệ thống tự đẩy cập nhật về, không cần hỏi lại liên tục. Luồng tự đóng khi job kết thúc.

Ví dụ code cho Theo dõi tiến độ realtime

GET /v1/jobs/{id}/events
curl -X GET https://api.shopapi.vn/v1/jobs/<id>/events \
  -H "Authorization: Bearer sk_live_XXXXXXXXXXXX"
Phản hồi200
event: job.progress
data: {"job_id":"job_x7k2m9p4qr8s","status":"running","progress":45,"stage":"generating","message":"Đang tạo đoạn 3/7","eta_seconds":30}

event: job.succeeded
data: {"job_id":"job_x7k2m9p4qr8s","status":"succeeded","progress":100,"cost":"957000000"}

Tham chiếu API

Ví và thanh toán

Số dư, sao kê, nạp tiền bằng mã QR ngân hàng.

Số dư

Tiền mặt trong ví và các gói còn hạn.

GET/v1/balance
API key hoặc đăng nhập

Mọi số tiền là micro-VND dạng chuỗi: `1 đồng = 1.000.000 µVND`. Chia cho 1.000.000 để ra số đồng. Dùng chuỗi vì JavaScript không biểu diễn chính xác số nguyên lớn.

Lỗi hay gặp

Ví dụ code cho Số dư

GET /v1/balance
curl -X GET https://api.shopapi.vn/v1/balance \
  -H "Authorization: Bearer sk_live_XXXXXXXXXXXX"
Phản hồi200
{
  "wallet": "2500000000000",
  "entitlements": [
    {
      "type": "voice_seconds",
      "remaining": 648000,
      "expires_at": "2027-08-03T00:00:00Z"
    }
  ],
  "estimated": {
    "voice_minutes": 22500,
    "images": 25000,
    "videos": 5000
  }
}

Thống kê chi tiêu

Tổng hợp chi tiêu theo ngày hoặc theo loại dịch vụ.

GET/v1/usage
API key hoặc đăng nhập

Tham số trên query string

Tham sốKiểuMô tả
fromTuỳ chọnstringNgày bắt đầu, dạng YYYY-MM-DD.
toTuỳ chọnstringNgày kết thúc, dạng YYYY-MM-DD.
group_byTuỳ chọnstringMặc định: daydaytypeGom nhóm theo ngày hay theo loại dịch vụ.

Ví dụ code cho Thống kê chi tiêu

GET /v1/usage
curl -X GET https://api.shopapi.vn/v1/usage \
  -H "Authorization: Bearer sk_live_XXXXXXXXXXXX"
Phản hồi200
{
  "object": "usage",
  "group_by": "day",
  "total_spend": "458000000000",
  "total_jobs": 1204,
  "buckets": [
    {
      "key": "2026-08-03",
      "spend": "32000000000",
      "jobs": 87
    }
  ]
}

Sao kê sổ cái

Từng bút toán thay đổi số dư, không bao giờ bị sửa hay xoá.

GET/v1/ledger
API key hoặc đăng nhập

Số dư ví của bạn luôn bằng đúng tổng của sổ cái này. Mọi khoản tạm giữ, trừ tiền, hoàn tiền đều là một dòng riêng, nên bạn tự đối soát được đến từng đồng.

Tham số trên query string

Tham sốKiểuMô tả
limitTuỳ chọnintegerMặc định: 501..100Số dòng mỗi trang.
cursorTuỳ chọnstringCon trỏ trang tiếp theo.

Lỗi hay gặp

Ví dụ code cho Sao kê sổ cái

GET /v1/ledger
curl -X GET https://api.shopapi.vn/v1/ledger \
  -H "Authorization: Bearer sk_live_XXXXXXXXXXXX"
Phản hồi200
{
  "object": "list",
  "data": [
    {
      "id": "led_a1b2c3d4e5f6",
      "type": "CAPTURE",
      "amount": "-957000000",
      "balance_after": "2499043000000",
      "description": "Trừ tiền job job_x7k2m9p4qr8s"
    }
  ],
  "has_more": true,
  "next_cursor": "led_a1b2c3d4e5f6"
}

Tạo yêu cầu nạp tiền

Sinh mã QR VietQR và nội dung chuyển khoản.

POST/v1/topup/intent
Cần đăng nhập

Bạn quét QR bằng app ngân hàng, tiền vào ví tự động trong khoảng 10 giây. Nội dung chuyển khoản phải giữ nguyên để hệ thống nhận ra đúng tài khoản của bạn.

Tham số trong body

Tham sốKiểuMô tả
amountBắt buộcstringSố tiền nạp, đơn vị micro-VND dạng chuỗi.

Lỗi hay gặp

Ví dụ code cho Tạo yêu cầu nạp tiền

POST /v1/topup/intent
curl -X POST https://api.shopapi.vn/v1/topup/intent \
  -H "Authorization: Bearer sk_live_XXXXXXXXXXXX" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
  "amount": "500000000000"
}'
Phản hồi200
{
  "id": "txn_p9q8r7s6t5u4",
  "object": "topup_intent",
  "status": "pending",
  "amount": "500000000000",
  "bonus": "25000000000",
  "credited": "525000000000",
  "bonus_percent": 5,
  "transfer_content": "SHOPAPI usr7k2m9p4",
  "qr_image_url": "https://img.vietqr.io/image/MB-0123456789-compact2.png",
  "expires_at": "2026-08-03T11:00:00Z"
}

Kiểm tra nạp tiền

Hỏi xem tiền đã vào chưa.

GET/v1/topup/{txn_id}
Cần đăng nhập

Giao diện web hỏi lại mỗi 3 giây cho tới khi `status` chuyển sang `succeeded`.

Lỗi hay gặp

Ví dụ code cho Kiểm tra nạp tiền

GET /v1/topup/{txn_id}
curl -X GET https://api.shopapi.vn/v1/topup/<txn_id> \
  -H "Authorization: Bearer sk_live_XXXXXXXXXXXX"
Phản hồi200
{
  "id": "txn_p9q8r7s6t5u4",
  "status": "succeeded",
  "credited": "525000000000",
  "paid_at": "2026-08-03T10:32:41Z"
}

Tham chiếu API

Bảng giá

Xem giá và báo giá trước, không cần đăng nhập.

Bảng giá

Giá hiện hành của cả ba dịch vụ. Không cần đăng nhập.

GET/v1/pricing
Công khai, không cần key

Ví dụ code cho Bảng giá

GET /v1/pricing
curl -X GET https://api.shopapi.vn/v1/pricing
Phản hồi200
{
  "object": "pricing",
  "currency": "VND",
  "rules": [
    {
      "type": "tts",
      "unit": "audio_second",
      "unit_price": "3333333",
      "display_price": "200₫",
      "display_unit": "phút"
    },
    {
      "type": "image",
      "unit": "image",
      "unit_price": "100000000",
      "display_price": "100₫",
      "display_unit": "ảnh"
    },
    {
      "type": "video",
      "unit": "video",
      "unit_price": "500000000",
      "display_price": "500₫",
      "display_unit": "video"
    }
  ]
}

Báo giá trước

Biết trước job sẽ tốn bao nhiêu, trước khi bấm chạy.

POST/v1/pricing/estimate
Công khai, không cần key

Tham số trong body

Tham sốKiểuMô tả
typeBắt buộcstringttsimagevideoLoại dịch vụ.
text_lengthTuỳ chọnintegerSố ký tự — chỉ dùng với `type: "tts"`.
nTuỳ chọnintegerSố ảnh — chỉ dùng với `type: "image"`.
durationTuỳ chọnintegerĐộ dài video — chỉ dùng với `type: "video"`.

Ví dụ code cho Báo giá trước

POST /v1/pricing/estimate
curl -X POST https://api.shopapi.vn/v1/pricing/estimate \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
  "type": "tts",
  "text_length": 4482
}'
Phản hồi200
{
  "object": "pricing.estimate",
  "type": "tts",
  "estimated_cost": "1200000000",
  "likely_cost": "958000000",
  "estimated_seconds": 45,
  "breakdown": {
    "unit": "phút",
    "quantity": 6,
    "unit_price": "200000000"
  }
}

Hướng dẫn

Vòng đời job

Job đi một chiều qua bảy trạng thái. Biết job đang ở đâu là biết tiền của bạn đang ở đâu.

Bảy trạng thái, và job chỉ đi một chiều — không bao giờ quay ngược về trạng thái cũ.
Đang xếp hàngĐang chạyHoàn thànhĐường đi thuận lợi
  • Đang chạyĐang thử lạiĐang chạyTrục trặc tạm thời, hệ thống tự thử lại
  • Đang chạyThất bạiHết cách — đã hoàn tiền
  • Đang chạyĐã huỷBạn huỷ — đã hoàn tiền
  • Đang xếp hàngBị từ chốiNội dung vi phạm — đã hoàn tiền
Trạng tháiNghĩa là gìTiền của bạnĐã kết thúc
queuedĐang xếp hàngJob đã nhận, đang chờ máy rảnhĐang tạm giữ theo ước tínhChưa, còn chạy tiếp
runningĐang chạyMáy đang xử lý job của bạnVẫn đang tạm giữChưa, còn chạy tiếp
retryingĐang thử lạiGặp trục trặc tạm thời, hệ thống tự thử lạiVẫn đang tạm giữChưa, còn chạy tiếp
succeededHoàn thànhKết quả đã sẵn sàng để tải vềTrừ đúng phần đã dùng, phần thừa hoàn lại víCó, không đổi được nữa
failedThất bạiĐã hoàn lại toàn bộ tiền tạm giữĐã hoàn tiền toàn bộCó, không đổi được nữa
cancelledĐã huỷBạn đã huỷ, tiền đã hoàn lại đầy đủĐã hoàn tiền toàn bộCó, không đổi được nữa
rejectedBị từ chốiNội dung vi phạm quy định, tiền đã hoàn lạiĐã hoàn tiền toàn bộCó, không đổi được nữa

Ba trạng thái nào cũng được hoàn tiền đầy đủ

failed, cancelledrejected luôn kèm trường refunded bằng đúng số đã tạm giữ, và cost bằng "0". Bạn không cần làm gì để được hoàn — tiền về ví ngay khi job chuyển trạng thái.

Theo dõi realtime bằng SSE

Mở một kết nối, hệ thống tự đẩy tiến độ về. Nhẹ hơn nhiều so với hỏi lại liên tục.

GET /v1/jobs/{id}/events trả về luồng Server-Sent Events — một kết nối HTTP giữ mở, mỗi khi job có thay đổi thì một dòng dữ liệu được đẩy về. Luồng tự đóng khi job kết thúc, bạn không cần dọn dẹp gì thêm.

Mỗi sự kiện có tên (job.progress, job.succeeded, job.failed) và phần dữ liệu JSON kèm progress theo phần trăm, stage, message tiếng Việt và eta_seconds.

Nếu ứng dụng của bạn chạy ở phía máy chủ và không cần thấy tiến độ từng bước, dùng webhook sẽ đơn giản hơn: bạn không phải giữ kết nối nào cả.

Ví dụ theo dõi realtime

GET /v1/jobs/{id}/events
curl -N https://api.shopapi.vn/v1/jobs/job_x7k2m9p4qr8s/events \
  -H "Authorization: Bearer sk_live_XXXXXXXXXXXX"
Luồng nhận được
event: job.progress
data: {"job_id":"job_x7k2m9p4qr8s","status":"running","progress":45,"stage":"generating","message":"Đang tạo đoạn 3/7","eta_seconds":30}

event: job.succeeded
data: {"job_id":"job_x7k2m9p4qr8s","status":"succeeded","progress":100,"cost":"957000000"}

Webhook

Khai webhook_url lúc tạo job, hệ thống sẽ gọi về máy chủ của bạn khi job xong.

Bạn đưa webhook_url vào body lúc tạo job. Khi có chuyện xảy ra, chúng tôi gửi một yêu cầu POST tới địa chỉ đó, kèm chữ ký để bạn kiểm chứng.

Sự kiệnNghĩa là gìKhi nào bạn nhận được
job.succeededJob chạy xongKết quả đã sẵn sàng. Đây là sự kiện bạn cần xử lý chính.
job.failedJob thất bạiJob hỏng hoặc bị từ chối. Tiền tạm giữ đã hoàn về ví của bạn.
job.progressTiến độ jobChỉ gửi khi bạn bật trong phần cài đặt webhook. Job dài sẽ bắn nhiều lần.

Header trong mỗi lần gọi

POST https://ban-cua-ban.vn/webhooks/shopapi
X-ShopAPI-Signature: t=1785312000,v1=8f3c1d0a5b7e2c94a1f6d8b3e0c7a495
X-ShopAPI-Event: job.succeeded
Content-Type: application/json

t là dấu thời gian Unix lúc ký, v1 là mã HMAC-SHA256 của chuỗi "<t>.<body thô>" với khoá bí mật webhook của bạn.

Kiểm tra chữ ký

Bắt buộc kiểm tra chữ ký trước khi tin vào nội dung, vì địa chỉ webhook của bạn là công khai — bất kỳ ai cũng có thể gửi dữ liệu giả tới đó. Ba điểm dễ sai:

  • Ký trên body thô, không phải body đã qua JSON.parse rồi đóng gói lại — chỉ cần lệch một dấu cách là chữ ký khác.
  • So sánh bằng hàm chống đo thời gian (hmac.compare_digest, timingSafeEqual, hash_equals), đừng dùng dấu bằng thường.
  • Từ chối những lần gọi có t quá cũ (ví dụ hơn 5 phút) để chặn việc phát lại yêu cầu cũ.

Lịch thử lại

Nếu máy chủ của bạn không trả về mã 2xx, chúng tôi gọi lại theo lịch dưới đây. Sau 5 lần thất bại thì dừng hẳn, và lần gọi hỏng được ghi vào nhật ký webhook để bạn xem lại trong bảng điều khiển.

  1. Lần 1ngay lập tức
  2. Lần 230 giây
  3. Lần 32 phút
  4. Lần 410 phút
  5. Lần 51 giờ

Trả 200 ngay, xử lý sau

Máy chủ của bạn nên ghi nhận sự kiện rồi trả 200 trong vòng vài giây, và đẩy phần việc nặng (tải file, ghi cơ sở dữ liệu, gửi mail) sang hàng đợi chạy nền. Xử lý lâu quá thì lần gọi bị coi là thất bại và phải chờ thử lại. Ngoài ra, hãy chống trùng theo job.id: một job có thể được báo nhiều lần (thử lại, hoặc bạn bật thêm job.progress), nên xử lý phải cho ra cùng một kết quả dù chạy bao nhiêu lần.

Ví dụ kiểm tra chữ ký webhook

Kiểm tra chữ ký
# Webhook được ký bằng HMAC-SHA256. Header có dạng:
#   X-ShopAPI-Signature: t=1785312000,v1=<chữ ký>
#   X-ShopAPI-Event: job.succeeded
#
# Chuỗi được ký là: "<t>.<toàn bộ body dạng thô>"
# Bạn phải so sánh bằng hàm so sánh chống đo thời gian.
Body bạn nhận được
{
  "event": "job.succeeded",
  "created_at": "2026-08-03T10:31:12Z",
  "data": {
    "job": {
      "id": "job_x7k2m9p4qr8s",
      "status": "succeeded",
      "progress": 100,
      "output": {
        "url": "https://cdn.shopapi.vn/o/2026/08/03/x7k2m9.mp3",
        "expires_at": "2026-08-10T10:30:00Z",
        "size_bytes": 2847293,
        "duration_seconds": 287.4,
        "format": "mp3"
      },
      "usage": {
        "characters": 4482,
        "audio_seconds": 287.4
      },
      "cost": "957000000",
      "refunded": "43000000",
      "created_at": "2026-08-03T10:30:00Z",
      "completed_at": "2026-08-03T10:31:12Z"
    }
  }
}

Bảng mã lỗi

Mọi lỗi đều theo cùng một khuôn, và thông điệp luôn nói bạn cần làm gì tiếp theo.

Khi có lỗi, phần thân phản hồi luôn là một object error với các trường code (mã máy đọc), message (câu tiếng Việt cho người đọc), type, param (tham số gây lỗi, nếu xác định được) và request_id.

Bạn hãy rẽ nhánh xử lý theo code, đừng dò chữ trong message — câu chữ có thể được viết lại cho dễ hiểu hơn, còn code thì không đổi. Khi cần hỗ trợ, gửi kèm request_id để chúng tôi tra đúng lần gọi đó.

HTTPMã lỗiÝ nghĩaBạn nên làm gì
400invalid_requestinvalid_request_errorCần sửa rồi gửi lạiYêu cầu chưa hợp lệCó tham số bị thiếu hoặc sai định dạng nên chúng tôi chưa xử lý được.Bạn kiểm tra lại các ô đã nhập, hoặc xem mục tương ứng trong tài liệu API.
401invalid_api_keyauthentication_errorCần sửa rồi gửi lạiAPI key không dùng đượcBạn chưa tạo API key, hoặc key đang dùng bị sai, đã thu hồi, hay dán thiếu một phần.Vào Bảng điều khiển → API key để tạo key (bấm "Tạo key mới"), rồi dán vào code. Key chỉ hiện đúng một lần lúc tạo — chép ngay, mất thì tạo key khác chứ không xem lại được.
402insufficient_balancebilling_errorThử lại đượcSố dư không đủVí của bạn chưa đủ tiền để chạy yêu cầu này. Chúng tôi chưa trừ đồng nào và job cũng chưa được tạo.Bạn nạp tiền bằng mã QR ở mục Nạp tiền — tối thiểu 10.000đ, tiền vào ví trong khoảng 10 giây. Giá: 200đ mỗi phút giọng đọc, 100đ mỗi ảnh, 500đ mỗi video; job hỏng hoàn 100% tiền.
403content_rejectedpermission_errorCần sửa rồi gửi lạiNội dung không được phépNội dung bạn gửi vi phạm quy định sử dụng nên chúng tôi phải từ chối. Tiền đã được hoàn lại đầy đủ.Bạn sửa lại nội dung rồi gửi lại giúp mình.
403permission_deniedpermission_errorCần sửa rồi gửi lạiKhoá API không có quyền nàyKhoá bạn đang dùng bị giới hạn phạm vi, giới hạn địa chỉ IP, hoặc đã chạm hạn mức chi tiêu tháng. Trường `reason` cho biết cụ thể là lý do nào. Ví của bạn không bị trừ.Bạn vào mục API key để nới ràng buộc, hoặc dùng khoá khác. Nếu không phải bạn gọi thì hãy thu hồi khoá này ngay.
403account_suspendedpermission_errorCần sửa rồi gửi lạiTài khoản đang tạm khoáTài khoản của bạn đang bị tạm khoá nên không tạo được yêu cầu mới.Bạn kiểm tra email chúng tôi đã gửi, hoặc liên hệ hỗ trợ để mở lại.
404not_foundinvalid_request_errorCần sửa rồi gửi lạiKhông tìm thấyMục bạn đang tìm không tồn tại hoặc không thuộc tài khoản này.Bạn kiểm tra lại đường dẫn, hoặc quay về danh sách để chọn lại.
409conflictinvalid_request_errorCần sửa rồi gửi lạiTrạng thái đã thay đổiThao tác này không còn hợp lệ với trạng thái hiện tại — ví dụ bạn huỷ một job vừa chạy xong.Bạn tải lại để xem trạng thái mới nhất rồi thao tác lại.
409idempotency_conflictidempotency_errorCần sửa rồi gửi lạiTrùng mã chống lặpBạn dùng lại một Idempotency-Key cũ nhưng nội dung gửi lên đã khác. Chúng tôi không xử lý để tránh tạo trùng job.Bạn đổi sang một Idempotency-Key mới (khuyến nghị dùng uuid) rồi gửi lại.
422unsupported_parameterinvalid_request_errorCần sửa rồi gửi lạiTham số chưa được hỗ trợGiá trị bạn chọn không nằm trong danh sách engine này chấp nhận.Ví dụ Veo3 chỉ nhận video 8 giây, Seedance nhận 5 hoặc 10 giây. Bạn chọn lại giúp mình.
429rate_limit_exceededrate_limit_errorThử lại đượcBạn gửi hơi nhanhSố yêu cầu vượt quá giới hạn của hạng tài khoản hiện tại.Bạn chờ vài giây rồi thử lại. Cần chạy nhiều hơn thì nâng hạng ở mục Bảng giá.
500internal_errorapi_errorThử lại đượcLỗi từ phía chúng tôiCó sự cố bên hệ thống. Nếu đã tạm giữ tiền thì sẽ được hoàn lại tự động.Bạn thử lại giúp mình. Nếu vẫn lỗi, gửi mã request_id cho hỗ trợ để tra cứu nhanh.
503engine_unavailableapi_errorThử lại đượcHệ thống đang quá tảiCụm xử lý tạm thời bận. Bạn không bị trừ tiền — toàn bộ đã được hoàn lại.Bạn thử lại sau khoảng một phút, hoặc để engine ở chế độ "auto" để hệ thống tự chọn máy rảnh.
503service_unavailableapi_errorThử lại đượcDịch vụ tạm gián đoạnMột thành phần hạ tầng đang có sự cố nên chúng tôi tạm ngừng nhận yêu cầu mới. Ví của bạn không bị ảnh hưởng.Bạn thử lại sau vài phút. Xem tình trạng hệ thống tại status.shopapi.vn để biết khi nào khôi phục.

Khuôn dạng body lỗi

Khuôn dạng lỗi chuẩn
{
  "error": {
    "code": "insufficient_balance",
    "message": "Số dư không đủ. Bạn cần nạp thêm 1.000đ để chạy job này.",
    "type": "billing_error",
    "param": null,
    "request_id": "req_9f3k2m7pq1x4",
    "required": "1000000000",
    "available": "250000000"
  }
}

Lỗi cấp job

Job hỏng sau khi đã nhận thì lỗi nằm trong chính đối tượng job, không phải mã HTTP.

Lời gọi tạo job thành công (mã 202) nhưng job vẫn có thể hỏng lúc chạy. Khi đó status chuyển thành failed và trường error chứa code, message tiếng Việt và retryable. Danh sách mã cấp job rộng hơn mã HTTP vì nó mô tả chuyện xảy ra bên trong máy xử lý.

Mọi lỗi cấp job đều đã hoàn tiền. Với các mã retryable, bạn tạo lại job là chạy tiếp được — hệ thống sẽ chọn máy khác.

Mã lỗi jobÝ nghĩaBạn nên làm gì
engine_errorChạy lại đượcMáy xử lý gặp lỗiEngine chạy nhưng không ra kết quả hợp lệ. Toàn bộ tiền tạm giữ đã hoàn về ví.Bạn bấm "Chạy lại" — hệ thống sẽ tự chọn một máy khác.
engine_unavailableChạy lại đượcKhông còn máy rảnhCả cụm engine đang bận hoặc tạm nghỉ. Bạn không bị trừ tiền.Bạn thử lại sau ít phút, hoặc để engine ở chế độ "auto".
content_rejectedCần sửa nội dungNội dung bị từ chốiNội dung vi phạm quy định nên job bị dừng. Tiền đã hoàn lại đầy đủ.Bạn sửa lại nội dung rồi tạo job mới.
timeoutChạy lại đượcQuá thời gian chờJob chạy lâu hơn mức cho phép nên bị dừng. Tiền đã hoàn lại đầy đủ.Với văn bản dài, bạn thử chia thành nhiều job nhỏ hơn.
download_failedChạy lại đượcKhông tải được kết quảEngine có ra file nhưng hệ thống không lấy về được. Tiền đã hoàn lại.Bạn bấm "Chạy lại" giúp mình.
upload_failedChạy lại đượcKhông lưu được kết quảFile tạo xong nhưng lưu trữ gặp sự cố. Tiền đã hoàn lại.Bạn bấm "Chạy lại" giúp mình.
account_exhaustedChạy lại đượcHết lượt trong ngàyTài nguyên cho loại job này đã dùng hết hạn mức hôm nay. Bạn không bị trừ tiền.Bạn thử lại sau 00:00, hoặc đổi sang engine khác.
cancelled_by_userChạy lại đượcBạn đã huỷ job nàyJob dừng theo yêu cầu của bạn. Toàn bộ tiền tạm giữ đã trả về ví.Bạn có thể tạo lại job bất cứ lúc nào.
internal_errorChạy lại đượcLỗi hệ thốngSự cố bên phía chúng tôi. Tiền tạm giữ đã hoàn lại tự động.Bạn thử lại, nếu vẫn lỗi thì gửi mã job cho hỗ trợ.

Danh mục giọng đọc

Sáu giọng Việt, đủ ba miền. Bạn truyền voice_id vào body khi tạo job giọng nói.

voice_idTên giọngGiới tínhVùng miềnHợp dùng cho
vi_female_01Ngọc AnhNữMiền BắcNữ miền Bắc, trong trẻo — hợp đọc tin tức, thuyết minh
vi_female_02Thu HàNữMiền BắcNữ miền Bắc, trầm ấm — hợp kể chuyện, audiobook
vi_male_01Minh QuânNamMiền BắcNam miền Bắc, chắc khoẻ — hợp quảng cáo, giới thiệu
vi_female_03Mỹ DuyênNữMiền NamNữ miền Nam, gần gũi — hợp review, TikTok
vi_male_02Hoàng NamNamMiền NamNam miền Nam, thân thiện — hợp video bán hàng
vi_female_04Diệu LinhNữMiền TrungNữ miền Trung, nhẹ nhàng — hợp nội dung du lịch

Không truyền voice_id thì hệ thống dùng mặc định vi_female_01. Bạn nghe thử từng giọng ở trang Playground trước khi đưa vào code.

Tài liệu API — ShopAPI · ShopAPI