Ba lệnh gọi biến một ảnh thành một file
Mỗi lệnh đọc khoá của bạn từ biến môi trường OPTRACY_API_KEY. Hãy tạo khoá ở trang tài khoản trước.
Gửi ảnh kèm loại file bạn muốn. Câu trả lời đến ngay, có id của việc và tiêu đề Retry-After.
curl
curl -X POST https://api.optracy.com/v1/jobs \
-H "Authorization: Bearer $OPTRACY_API_KEY" \
-H "Idempotency-Key: design-001" \
-F [email protected] -F format=svg
JSON
{
"id": "job_8f14e45fceea167a5a36dedd4bea2543",
"status": "queued",
"phase": "queued",
"progress": 0,
"created_at": "2026-10-06T08:00:00Z",
"finished_at": null,
"options": {
"format": "svg",
"size": "original",
"colors": null,
"background": "keep"
},
"credits_charged": 0,
"result": null,
"error": null
}
Hỏi lại việc sau số giây mà Retry-After cho, tới khi status là succeeded.
curl
curl https://api.optracy.com/v1/jobs/job_8f14e45fceea167a5a36dedd4bea2543 \
-H "Authorization: Bearer $OPTRACY_API_KEY"
Tải file về.
curl
curl -o design.svg https://api.optracy.com/v1/jobs/job_8f14e45fceea167a5a36dedd4bea2543/result \
-H "Authorization: Bearer $OPTRACY_API_KEY"
Mọi lệnh gọi đều mang khoá API của bạn
Gửi khoá trong tiêu đề Authorization: Authorization: Bearer optr_…. Khoá dài 43 ký tự và bắt đầu bằng optr_. Bạn tạo khoá ở trang tài khoản, tối đa 5 khoá cùng lúc, và mỗi khoá chỉ hiện một lần.
Chỉ để khoá trên máy chủ của bạn. Đừng đặt khoá trong trang web, ứng dụng điện thoại hay kho mã công khai: ai có khoá là tiêu được credit của bạn. Nếu khoá bị lộ, hãy thu hồi ở trang tài khoản; khoá ngừng hoạt động ngay.
Thiếu khoá, sai khoá hay khoá đã thu hồi nhận 401; khoá của tài khoản bị khoá nhận 403. Một mạng gửi 30 khoá sai trong một phút thì lệnh gọi sai khoá tiếp theo từ mạng đó nhận 429 tới hết phút; khoá đúng vẫn dùng được.
Mở trang tài khoản
Một credit đổi được một ảnh và một file
Một việc thành công tốn 1 credit và cho bạn một file. Trong lúc việc chạy, credit của nó được giữ lại, nên bạn không bao giờ gửi quá số việc mình trả được; credit chỉ bị trừ khi file đã xong.
Việc bị lỗi, bị bạn huỷ hoặc bị ngắt vì máy chủ khởi động lại thì không tốn gì: credit đang giữ được trả lại. Credit không hết hạn.
Credit chỉ dùng cho API. Lượt tải trên trang web được tính riêng: tài khoản miễn phí tải được 5 ảnh mỗi ngày, gói tháng tải không giới hạn, và cả hai đều không dùng credit. Xem bảng giá
Credit được cộng bằng tay. Muốn có credit, hãy viết thư tới [email protected]
GET /v1/account cho biết số dư, số credit đang giữ cho việc chưa xong, số credit dùng được và số việc chưa xong tài khoản của bạn được có cùng lúc (active_jobs_max).
Một việc chờ, chạy, rồi kết thúc theo một trong ba cách
Mỗi việc có một trong năm trạng thái. queued và running là chưa xong; ba trạng thái còn lại là cuối cùng và không đổi nữa. Thời gian theo UTC, dạng ISO 8601.
Khi việc chưa xong, phase là queued, analysing, building hoặc finishing. Chỉ dùng nó để hiện tiến độ; các giai đoạn có thể đổi mà không báo trước.
error.code của một việc lỗi là một trong hai mã sau:
processing_failed: Không chuyển được ảnh này thành file. Hãy thử ảnh khác hoặc tuỳ chọn khác.
interrupted: Máy chủ khởi động lại làm việc dừng giữa chừng và không chạy lại được. Hãy gửi lại ảnh.
Bốn tuỳ chọn quyết định file bạn nhận
Gửi các tuỳ chọn là các trường của cùng yêu cầu multipart/form-data với ảnh. Trường mà API không biết sẽ bị từ chối, nên gõ sai tên trường không bao giờ bị bỏ qua lặng lẽ.
Gửi lại một yêu cầu không bao giờ tạo việc thứ hai
Lỗi mạng là chuyện thường. Hãy gửi tiêu đề Idempotency-Key với mỗi lần gửi ảnh và giữ nguyên khoá đó khi gửi lại: cùng ảnh, cùng tuỳ chọn, cùng khoá thì nhận lại việc đầu tiên, có dấu Idempotent-Replayed: true, và credit không bị giữ hai lần. Khoá được nhớ trong 24 giờ.
Cùng khoá mà khác ảnh hoặc khác tuỳ chọn thì nhận 422 idempotency_key_reused. Yêu cầu bị từ chối (sai trường, thiếu credit, vượt giới hạn) không giữ khoá, nên bạn gửi lại được khi đã sửa xong.
Giới hạn tính theo tài khoản và theo phút
Vượt giới hạn thì nhận 429 kèm Retry-After: chờ đúng số giây đó rồi gửi lại.
Số việc chưa xong cùng lúc được đặt riêng cho từng tài khoản: mặc định 3, nâng lên 5 hoặc 10 khi bạn yêu cầu (viết thư tới [email protected]). GET /v1/account trả số của tài khoản bạn trong active_jobs_max, và câu từ chối too_many_active_jobs cũng nêu số đó trong detail.
Không có cam kết về thời gian xử lý. Một việc mất từ vài giây tới vài phút, tuỳ ảnh và tuỳ máy chủ đang bận tới đâu.
File của bạn bị xoá sau 24 giờ
Ảnh bạn gửi chỉ được giữ trong lúc việc chạy và bị xoá khi việc kết thúc. File được giữ 24 giờ sau khi việc thành công rồi bị xoá, nên hãy tải về trong thời gian đó. Sau đó, lệnh tải trả 410 result_expired; bản thân việc vẫn đọc được.
Trang giữ gì về tài khoản của bạn: Quyền riêng tư
Mỗi lỗi có một mã cố định và một việc nên làm
Lỗi trả về dạng application/problem+json (RFC 9457): type trỏ tới mục tương ứng bên dưới, rồi title, status, detail và code, trường nên dựa vào. Sai trường thì có thêm param. Mọi câu trả lời 429 và 503 đều có Retry-After.
JSON
{
"type": "https://optracy.com/docs/api#error-insufficient_credits",
"title": "Not enough credits",
"status": 402,
"detail": "This job costs 1 credit; 0 are available.",
"code": "insufficient_credits"
}
Sáu địa chỉ làm nên toàn bộ API
Mọi địa chỉ bắt đầu bằng https://api.optracy.com. Mô tả đầy đủ cho công cụ của bạn là tài liệu OpenAPI 3.1 ở /v1/openapi.json
POST /v1/jobs
Gửi một ảnh. Gửi một ảnh kèm tuỳ chọn của file bạn muốn. Câu trả lời đến ngay với việc vừa tạo (202); file được làm ở phía sau. 1 credit được giữ trong lúc việc chạy và chỉ bị trừ khi việc thành công. Mỗi tài khoản được có 3 việc chưa xong cùng lúc, trừ khi đã được nâng số này (active_jobs_max của GET /v1/account).
Tham số
Câu trả lời: 202
JSON
{
"id": "job_8f14e45fceea167a5a36dedd4bea2543",
"status": "queued",
"phase": "queued",
"progress": 0,
"created_at": "2026-10-06T08:00:00Z",
"finished_at": null,
"options": {
"format": "svg",
"size": "original",
"colors": null,
"background": "keep"
},
"credits_charged": 0,
"result": null,
"error": null
}
Lỗi
GET /v1/jobs/{job_id}
Đọc một việc. Trạng thái của việc, giai đoạn và tiến độ khi đang chạy, kết quả hoặc lỗi khi đã kết thúc. Khi việc đang chạy, hãy hỏi lại sau số giây trong Retry-After.
Tham số
Câu trả lời: 200
JSON
{
"id": "job_8f14e45fceea167a5a36dedd4bea2543",
"status": "succeeded",
"phase": "finishing",
"progress": 100,
"created_at": "2026-10-06T08:00:00Z",
"finished_at": "2026-10-06T08:00:16Z",
"options": {
"format": "svg",
"size": "original",
"colors": null,
"background": "keep"
},
"credits_charged": 1,
"result": {
"format": "svg",
"width": 1086,
"height": 1448,
"bytes": 180651,
"colors": 13,
"background_removed": false,
"url": "https://api.optracy.com/v1/jobs/job_8f14e45fceea167a5a36dedd4bea2543/result",
"expires_at": "2026-10-07T08:00:16Z"
},
"error": null
}
Lỗi
POST /v1/jobs/{job_id}/cancel
Huỷ một việc. Dừng một việc đang chờ hoặc đang chạy; không bị trừ gì. Huỷ lại việc đã huỷ không đổi gì; việc đã thành công hoặc đã lỗi thì không huỷ được (409).
Tham số
Câu trả lời: 200
JSON
{
"id": "job_8f14e45fceea167a5a36dedd4bea2543",
"status": "canceled",
"phase": null,
"progress": null,
"created_at": "2026-10-06T08:00:00Z",
"finished_at": "2026-10-06T08:00:05Z",
"options": {
"format": "svg",
"size": "original",
"colors": null,
"background": "keep"
},
"credits_charged": 0,
"result": null,
"error": null
}
Lỗi
GET /v1/jobs/{job_id}/result
Tải file về. File của việc đã thành công, dạng tệp đính kèm. File được giữ 24 giờ sau khi việc xong; sau đó câu trả lời là 410.
Tham số
Câu trả lời: 200
Lỗi
GET /v1/account
Xem credit và giới hạn của bạn. Credit của tài khoản sở hữu khoá (số dư, số đang giữ cho việc chưa xong, số dùng được), số việc chưa xong và số việc được có cùng lúc, và các giới hạn áp dụng cho tài khoản.
Câu trả lời: 200
JSON
{
"credits_balance": 120,
"credits_held": 1,
"credits_available": 119,
"active_jobs": 1,
"active_jobs_max": 3,
"limits": {
"job_cost": 1,
"active_jobs": 3,
"submissions_per_minute": 10,
"reads_per_minute": 120,
"max_upload_bytes": 31457280,
"max_upload_pixels": 3000000,
"result_ttl_seconds": 86400,
"idempotency_ttl_seconds": 86400
}
}
Lỗi
GET /v1/openapi.json
Đọc bản mô tả này. Bản mô tả OpenAPI 3.1 của API, cho công cụ của bạn. Không cần khoá. Không cần khoá.
Câu trả lời: 200
Lỗi
Chương trình đầy đủ bằng Python và Node.js
Mỗi chương trình gửi một ảnh, chờ theo Retry-After rồi lưu file. Cả hai không cần cài gói nào, và đều được chạy với API trong các bài kiểm thử của chúng tôi.
Python
Python 3.8 trở lên, chỉ dùng thư viện chuẩn.
Python
"""Turn one image into a print file with the Optracy processing API.
OPTRACY_API_KEY=optr_... python api_example.py design.png svg design.svg
Python 3.8 or newer, standard library only. The key is read from the environment, never written in the code.
"""
import json
import os
import sys
import time
import urllib.error
import urllib.request
import uuid
API = os.environ.get('OPTRACY_API_BASE', 'https://api.optracy.com') + '/v1'
KEY = os.environ['OPTRACY_API_KEY']
def call(method, url, body=None, headers=None):
"""(status, headers, body) of one request, errors included."""
headers = dict(headers or {}, Authorization='Bearer ' + KEY)
request = urllib.request.Request(url, data=body, method=method, headers=headers)
try:
with urllib.request.urlopen(request, timeout=120) as answer:
return answer.status, answer.headers, answer.read()
except urllib.error.HTTPError as e:
return e.code, e.headers, e.read()
def form(fields, image_path):
"""A multipart/form-data body with the fields and the image."""
boundary = uuid.uuid4().hex
parts = [('--%s\r\nContent-Disposition: form-data; name="%s"\r\n\r\n%s\r\n' % (boundary, k, v)).encode()
for k, v in fields.items()]
with open(image_path, 'rb') as f:
parts.append(('--%s\r\nContent-Disposition: form-data; name="image"; filename="%s"\r\n'
'Content-Type: application/octet-stream\r\n\r\n'
% (boundary, os.path.basename(image_path))).encode() + f.read() + b'\r\n')
parts.append(('--%s--\r\n' % boundary).encode())
return b''.join(parts), 'multipart/form-data; boundary=' + boundary
def wait(headers):
time.sleep(int(headers.get('Retry-After') or 3))
def convert(image_path, file_format, out_path):
body, content_type = form({'format': file_format}, image_path)
# The same key on every try: a request sent again never starts a second job.
headers = {'Content-Type': content_type, 'Idempotency-Key': str(uuid.uuid4())}
for _ in range(5):
status, answer_headers, answer = call('POST', API + '/jobs', body, headers)
if status not in (429, 503):
break
wait(answer_headers)
job = json.loads(answer)
if status != 202:
sys.exit('refused: %s (%s)' % (job['code'], job['detail']))
while job['status'] in ('queued', 'running'):
wait(answer_headers)
status, answer_headers, answer = call('GET', API + '/jobs/' + job['id'])
if status == 200:
job = json.loads(answer)
elif status != 429:
sys.exit('could not read the job: %s' % json.loads(answer)['code'])
if job['status'] != 'succeeded':
sys.exit('job %s: %s' % (job['status'], (job['error'] or {}).get('detail', '')))
status, _, data = call('GET', job['result']['url'])
if status != 200:
sys.exit('could not download the file: %s' % json.loads(data)['code'])
with open(out_path, 'wb') as f:
f.write(data)
print('%s: %d bytes, %d colors, %d credit charged' % (out_path, len(data), job['result']['colors'],
job['credits_charged']))
if __name__ == '__main__':
if len(sys.argv) != 4:
sys.exit('usage: python api_example.py <image> <svg|pdf|eps|png> <file to write>')
convert(*sys.argv[1:])
shell
OPTRACY_API_KEY=optr_… python api_example.py design.jpg svg design.svg
Node.js
Node.js 18 trở lên.
JavaScript
// Turn one image into a print file with the Optracy processing API.
//
// OPTRACY_API_KEY=optr_... node api_example.mjs design.png svg design.svg
//
// Node.js 18 or newer, no package needed. The key is read from the environment, never written in the code.
import { randomUUID } from 'node:crypto';
import { readFile, writeFile } from 'node:fs/promises';
import { basename } from 'node:path';
const API = (process.env.OPTRACY_API_BASE || 'https://api.optracy.com') + '/v1';
const auth = { Authorization: 'Bearer ' + process.env.OPTRACY_API_KEY };
const wait = (answer) => new Promise((done) => setTimeout(done, 1000 * Number(answer.headers.get('Retry-After') || 3)));
const [imagePath, format, outPath] = process.argv.slice(2);
if (!outPath) throw new Error('usage: node api_example.mjs <image> <svg|pdf|eps|png> <file to write>');
const form = new FormData();
form.append('format', format);
form.append('image', new Blob([await readFile(imagePath)]), basename(imagePath));
// The same key on every try: a request sent again never starts a second job.
const headers = { ...auth, 'Idempotency-Key': randomUUID() };
let answer;
for (let i = 0; i < 5; i++) {
answer = await fetch(API + '/jobs', { method: 'POST', headers, body: form });
if (answer.status !== 429 && answer.status !== 503) break;
await wait(answer);
}
let job = await answer.json();
if (answer.status !== 202) throw new Error(`refused: ${job.code} (${job.detail})`);
while (job.status === 'queued' || job.status === 'running') {
await wait(answer);
answer = await fetch(`${API}/jobs/${job.id}`, { headers: auth });
if (answer.status === 200) job = await answer.json();
else if (answer.status !== 429) throw new Error('could not read the job: ' + (await answer.json()).code);
}
if (job.status !== 'succeeded') throw new Error(`job ${job.status}: ${job.error ? job.error.detail : ''}`);
const file = await fetch(job.result.url, { headers: auth });
if (file.status !== 200) throw new Error('could not download the file: ' + (await file.json()).code);
const data = Buffer.from(await file.arrayBuffer());
await writeFile(outPath, data);
console.log(`${outPath}: ${data.length} bytes, ${job.result.colors} colors, ${job.credits_charged} credit charged`);
shell
OPTRACY_API_KEY=optr_… node api_example.mjs design.jpg svg design.svg
/v1 chỉ thêm, không phá
Trong /v1, mọi thay đổi chỉ thêm vào: trường mới trong câu trả lời, tuỳ chọn mới không bắt buộc, mã lỗi mới. Hãy bỏ qua các trường bạn không biết. Thay đổi nào làm hỏng chương trình đang chạy sẽ ra ở địa chỉ mới, /v2.