Optracy

Chuyển ảnh thành file in ngay từ phần mềm của bạn

API của Optracy nhận một ảnh và trả lại một file: SVG, PDF, EPS hoặc PNG in 300 dpi. Bạn gửi ảnh, hỏi lại tới khi việc xong, rồi tải file về. Mỗi ảnh xử lý thành công tốn 1 credit.

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.

  1. 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
    }
  2. 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"
  3. 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.

Trạng tháiNghĩa
queuedĐang chờ tới lượt. Hỏi lại sau Retry-After (5 giây).
runningĐang được làm. phase và progress (0 tới 100) cho biết đã tới đâu. Hỏi lại sau Retry-After (3 giây).
succeededFile đã xong: result cho địa chỉ, kích thước và số màu của file. 1 credit đã bị trừ.
failedKhông làm được file: error cho biết lý do. Không bị trừ gì.
canceledBạn đã huỷ, hoặc tài khoản bị khoá. Không bị trừ gì.

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ẽ.

Tên Bắt buộcMặc định Giá trịBạn nhận được gì
image có file Ảnh: PNG, JPEG, GIF, BMP hoặc WebP, tối đa 3 triệu điểm ảnh, mỗi cạnh ít nhất 16 px và cạnh dài không quá 20 lần cạnh ngắn. Cả yêu cầu tối đa 30 MB.
format có svg, pdf, eps, png File bạn nhận: svg, pdf, eps, hoặc png, một file PNG in 300 dpi.
size không original string original, hoặc <rộng>x<cao> tính bằng điểm ảnh: mỗi cạnh từ 1 tới 20.000 và tổng tối đa 40 triệu điểm ảnh. Thiết kế được đặt vừa trong khung, giữ nguyên tỉ lệ. File png với cỡ original là khung in 4.500 × 5.400 px ở 300 dpi.
dpi không 300 1..10000 Chỉ đi kèm size tường minh: độ phân giải ghi trong file, từ 1 tới 10.000.
colors không tự động 1..1000 Bỏ trống để tự động, hoặc số màu phẳng bạn muốn, từ 1 tới 1.000. Ảnh có ít màu hơn thì giữ số màu nó có; result.colors cho biết file có bao nhiêu màu.
background không keep keep, remove keep, hoặc remove: màu nền bị bỏ khỏi file vector và trong suốt trong file PNG. Nếu bỏ nền sẽ không còn gì thì không bỏ gì; result.background_removed cho biết đã xảy ra điều gì.

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.

Giá trịGiới hạn
1credit cho mỗi ảnh thành công
3việc chưa xong cùng lúc, đang chờ hoặc đang chạy (mặc định của mỗi tài khoản)
10ảnh gửi trong một phút
120lệnh gọi khác trong một phút
30khoá sai trong một phút từ một mạng
30MB cho cả một yêu cầu
3triệu điểm ảnh cho một ảnh
16px tối thiểu cho mỗi cạnh của ảnh
24giờ giữ file sau khi việc thành công
24giờ nhớ một Idempotency-Key

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"
}
MãHTTP NghĩaNên làm gì
unauthenticated401 Authentication requiredGửi một khoá hợp lệ dạng Authorization: Bearer <khoá>, và kiểm xem khoá chưa bị thu hồi.
account_disabled403 Account disabledTài khoản đang bị khoá. Hãy viết thư tới [email protected].
insufficient_credits402 Not enough creditsXin thêm credit, hoặc chờ một việc chưa xong kết thúc.
invalid_parameter400 Invalid parameterSửa trường mà param nêu tên; detail cho biết trường đó nhận giá trị nào.
image_too_large413 Image too largeGửi ảnh nhỏ hơn: tối đa 3 triệu điểm ảnh, và cả yêu cầu tối đa 30 MB.
unsupported_image422 Unsupported imageGửi file PNG, JPEG, GIF, BMP hoặc WebP mở được, mỗi cạnh ít nhất 16 px.
rate_limited429 Too many requestsChờ số giây trong Retry-After rồi gửi lại.
too_many_active_jobs429 Too many unfinished jobsChờ một việc của bạn kết thúc rồi gửi việc tiếp theo. detail nêu số việc tài khoản của bạn được có cùng lúc.
server_busy503 Server busyChờ số giây trong Retry-After rồi gửi lại với cùng Idempotency-Key.
idempotency_key_reused422 Idempotency key reusedDùng khoá mới cho ảnh mới hoặc tuỳ chọn mới.
not_found404 Not foundKiểm lại địa chỉ và mã việc. Việc của tài khoản khác cũng được trả lời là không có.
method_not_allowed405 Method not allowedDùng một phương thức có trong tiêu đề Allow.
result_not_ready409 Result not readyHỏi lại việc tới khi thành công. Việc lỗi hoặc đã huỷ thì không có file.
job_not_cancelable409 Job cannot be canceledKhông cần làm gì: việc đã kết thúc.
result_expired410 Result expiredFile đã bị xoá 24 giờ sau khi việc xong. Hãy gửi lại ảnh.
internal_error500 Internal errorThử lại. Nếu vẫn lỗi, hãy viết thư tới [email protected] kèm thời điểm gửi yêu cầu.

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ố

TênGửi trong Bắt buộcMặc định Giá trịBạn nhận được gì
Idempotency-Keytiêu đề không string Không bắt buộc. Từ 1 tới 255 chữ cái, chữ số và . _ : - (một UUID là được). Cùng ảnh, cùng tuỳ chọn, cùng khoá trong vòng 24 giờ thì nhận lại việc đầu tiên thay vì tạo việc mới.
imagetrường biểu mẫu có file Ảnh: PNG, JPEG, GIF, BMP hoặc WebP, tối đa 3 triệu điểm ảnh, mỗi cạnh ít nhất 16 px và cạnh dài không quá 20 lần cạnh ngắn. Cả yêu cầu tối đa 30 MB.
formattrường biểu mẫu có svg, pdf, eps, png File bạn nhận: svg, pdf, eps, hoặc png, một file PNG in 300 dpi.
sizetrường biểu mẫu không original string original, hoặc <rộng>x<cao> tính bằng điểm ảnh: mỗi cạnh từ 1 tới 20.000 và tổng tối đa 40 triệu điểm ảnh. Thiết kế được đặt vừa trong khung, giữ nguyên tỉ lệ. File png với cỡ original là khung in 4.500 × 5.400 px ở 300 dpi.
dpitrường biểu mẫu không 300 1..10000 Chỉ đi kèm size tường minh: độ phân giải ghi trong file, từ 1 tới 10.000.
colorstrường biểu mẫu không tự động 1..1000 Bỏ trống để tự động, hoặc số màu phẳng bạn muốn, từ 1 tới 1.000. Ảnh có ít màu hơn thì giữ số màu nó có; result.colors cho biết file có bao nhiêu màu.
backgroundtrường biểu mẫu không keep keep, remove keep, hoặc remove: màu nền bị bỏ khỏi file vector và trong suốt trong file PNG. Nếu bỏ nền sẽ không còn gì thì không bỏ gì; result.background_removed cho biết đã xảy ra điều gì.

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ố

TênGửi trong Bắt buộcMặc định Giá trịBạn nhận được gì
job_idđịa chỉ có string id của việc, như lần gửi đã trả về.

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ố

TênGửi trong Bắt buộcMặc định Giá trịBạn nhận được gì
job_idđịa chỉ có string id của việc, như lần gửi đã trả về.

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ố

TênGửi trong Bắt buộcMặc định Giá trịBạn nhận được gì
job_idđịa chỉ có string id của việc, như lần gửi đã trả về.

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.