API Reference
Submit Python files for protection and retrieve compiled artifacts programmatically. All API endpoints are under /api/v1/ and require an API key.
Overview
The PyVMProtect API lets you integrate Python protection into your own build pipeline. The typical flow is:
- Submit a
.pyor.zipfile → receive ajob_id - Poll the job status until
statusiscompleteorerror - (Optional) Confirm Cython hot-paths if status reaches
hot_path_pending - Download the protected artifact
Authentication
All /api/v1/ endpoints require an API key in the Authorization header:
Authorization: Bearer pvmp_your_api_key_here
To generate your API key, go to My Account and click Generate Key in the API Key section. Regenerating a key immediately invalidates the previous one.
Endpoints
/api/v1/job_submit.php
Upload a Python file or project zip and start a build job.
Request
Content-Type: multipart/form-data
| Field | Type | Description |
|---|---|---|
| file | file | Required. .py or .zip (max 32 MB; zip max 256 MB uncompressed) |
| module_name | string | Output module name. Defaults to main. Alphanumeric and underscores only. |
| options | JSON string | Optional build options object (see options below) |
| hot_paths | JSON array | Optional list of function names to pre-select for Cython protection |
Response 200
{
"job_id": "a3f9bc12e4561234a3f9bc12e4561234",
"status": "queued",
"module_name": "my_module",
"is_zip": false
}
/api/v1/job_status.php?job_id=<id>
Poll the current status of a build job. Call this every 1–3 seconds until status is complete or error.
Response 200
{
"job_id": "a3f9bc12e4561234...",
"status": "building",
"module_name": "my_module",
"is_zip": false,
"progress_pct": 45,
"progress_label": "Compiling extensions",
"created_at": 1713700000,
"updated_at": 1713700042,
// Present when status = "hot_path_pending":
"functions": ["encrypt_payload", "verify_license", "decode_config"],
// Present when status = "complete":
"download_token": "abc123..."
}
/api/v1/job_hotpath.php
Confirm which functions to Cython-compile when a job is in hot_path_pending state. Pass an empty array to skip Cython optimization and continue with standard protection.
Request body (JSON)
{
"job_id": "a3f9bc12e4561234...",
"hot_paths": ["encrypt_payload", "verify_license"]
}
Response 200
{ "ok": true, "job_id": "...", "hot_paths": ["encrypt_payload", "verify_license"] }
/api/v1/job_download.php?job_id=<id>
Download the protected artifact as a .zip file. Only available when status = complete. The artifact is deleted after the first successful download (one-time link, 1-hour TTL).
Response
Binary application/zip stream with Content-Disposition: attachment. On error, returns JSON.
Full Workflow
The complete lifecycle of a build job:
POST /api/v1/job_submit.php → { job_id }
↓
GET /api/v1/job_status.php?job_id= → status: queued
↓ (poll every 2s)
GET /api/v1/job_status.php?job_id= → status: building, progress_pct: 45
↓
GET /api/v1/job_status.php?job_id= → status: hot_path_pending, functions: [...]
↓ (optional — only if Cython is enabled)
POST /api/v1/job_hotpath.php → { ok: true }
↓
GET /api/v1/job_status.php?job_id= → status: complete, download_token: "..."
↓
GET /api/v1/job_download.php?job_id= → .zip download
Code Examples
curl
# 1. Submit a file curl -X POST https://pyvmprotect.com/api/v1/job_submit.php \ -H "Authorization: Bearer pvmp_your_key_here" \ -F "file=@my_script.py" \ -F "module_name=my_module" # 2. Poll status curl https://pyvmprotect.com/api/v1/job_status.php?job_id=JOB_ID \ -H "Authorization: Bearer pvmp_your_key_here" # 3. Download when complete curl -O https://pyvmprotect.com/api/v1/job_download.php?job_id=JOB_ID \ -H "Authorization: Bearer pvmp_your_key_here"
Python
import time
import requests
API_KEY = "pvmp_your_key_here"
BASE = "https://pyvmprotect.com/api/v1"
HEADERS = {"Authorization": f"Bearer {API_KEY}"}
# 1. Submit
with open("my_script.py", "rb") as f:
resp = requests.post(
f"{BASE}/job_submit.php",
headers=HEADERS,
files={"file": ("my_script.py", f, "text/x-python")},
data={"module_name": "my_module"},
)
resp.raise_for_status()
job_id = resp.json()["job_id"]
print(f"Job submitted: {job_id}")
# 2. Poll until done
while True:
status = requests.get(
f"{BASE}/job_status.php",
headers=HEADERS,
params={"job_id": job_id},
).json()
s = status["status"]
print(f" {s} ({status.get('progress_pct', 0)}%)")
if s == "hot_path_pending":
# Confirm all suggested functions (or pass a subset)
requests.post(
f"{BASE}/job_hotpath.php",
headers=HEADERS,
json={"job_id": job_id, "hot_paths": status["functions"]},
).raise_for_status()
elif s == "complete":
break
elif s == "error":
raise RuntimeError(f"Build failed: {status.get('error')}")
time.sleep(2)
# 3. Download
resp = requests.get(
f"{BASE}/job_download.php",
headers=HEADERS,
params={"job_id": job_id},
)
resp.raise_for_status()
with open("my_module_protected.zip", "wb") as f:
f.write(resp.content)
print("Downloaded: my_module_protected.zip")
Status Values
| Status | Meaning |
|---|---|
| queued | Job is waiting in the build queue |
| building | Daemon is actively compiling the submission |
| hot_path_pending | Awaiting your hot-path selection — call job_hotpath.php to continue |
| complete | Build succeeded — artifact available for download |
| error | Build failed — see the error field for details |
Rate Limits
| Endpoint | Limit |
|---|---|
| job_submit.php | 20 submissions per hour per account |
When a rate limit is exceeded the API returns HTTP 429 with an error message. Retry after the window resets (hourly).
Errors
All errors return JSON with an error field:
{ "error": "Invalid API key" }
| HTTP Code | Meaning |
|---|---|
400 | Bad request — missing or invalid parameter |
401 | Missing or invalid API key |
404 | Job not found (or belongs to another account) |
405 | Wrong HTTP method |
409 | Job is in the wrong state for this operation |
410 | Artifact already downloaded or link expired |
413 | File too large |
429 | Rate limit exceeded |
500 | Server error — contact support |