Docs / API
The HTTP API submits the same jobs the dashboard does, through the same pipeline, and debits the same credit balance. This page is the contract: what to send, what comes back, and what every status code means.
External requests authenticate with the X-API-Key header. External keys are only usable on an API plan; a request made with a key from a non-API account is rejected before it reaches the queue. Keys are issued from the dashboard, the full key is shown once at creation, and an account can hold five active keys at a time.
The dashboard needs no key handling because your session already carries an internal key, which is why dashboard submissions work on any active plan. Credits are shared between the two surfaces: a job created from the API debits the same balance a dashboard job would.
Never put an API key in client-side code, a repository, or a query string. Send it as the X-API-Key header from a server-side process and keep it in a secret manager or an environment variable. A key that appears in a browser bundle, a committed file or a URL is a key you should consider compromised and revoke.
POST /api/obfuscate takes a multipart form body and returns the job identifier, not the artifact. The build runs asynchronously, so the next step is always a poll.
| Field | Type | Required | Notes |
|---|---|---|---|
| file | multipart part | Required | The .py source to protect. Maximum 10 MB. |
| settings | JSON string | Required | The settings object serialised as a JSON string. Any key you leave out falls back to its default. |
| X-API-Key | request header | Required for external keys | The nyami_... key issued to your account. Dashboard requests carry an internal key instead. |
curl -X POST https://nyami.cc/api/obfuscate \
-H "X-API-Key: nyami_your_key_here" \
-F "file=@script.py" \
-F 'settings={"optimization":"1","python_version":"3.14","anti_debug":"none"}'{
"jobId": "clx0a1b2c3d4e5f6g7h8i9j0",
"message": "Job submitted successfully. Poll GET /api/obfuscate/[jobId] for status."
}The settings object accepts the full set of obfuscation keys. Omitted keys fall back to their defaults, so a minimal submission only needs to state what it changes. Every key, its accepted values and its default is listed on the settings page.
GET /api/obfuscate/{jobId} returns the current state of the job. Poll on a five second interval. A COMPLETED job carries the artifact size and a signed download URL; a FAILED job carries the failure detail instead.
| Field | Type | Notes |
|---|---|---|
| jobId | string | The identifier of the job. |
| status | string | PENDING, PROCESSING, COMPLETED or FAILED. |
| inputFileName | string | The name of the file you submitted. |
| inputFileSize | number | The size of the submitted file in bytes. |
| createdAt | string | When the job was created. |
| startedAt | string | When the pipeline picked the job up. |
| completedAt | string | When the job reached a terminal state. |
| outputFileSize | number | The size of the artifact in bytes. Present on a COMPLETED job. |
| downloadUrl | string | A signed URL for the artifact. Present on a COMPLETED job and valid for one hour. |
| errorMessage | string | The failure detail. Present on a FAILED job in place of the output fields. |
status takes one of four values.
| Status | Meaning |
|---|---|
| PENDING | The job is queued and has not started. It is counted against your plan's concurrent job ceiling. |
| PROCESSING | The pipeline is running. It is still counted against the ceiling. |
| COMPLETED | The artifact is ready. The response carries outputFileSize and a signed downloadUrl. |
| FAILED | The job ended without an artifact. The response carries errorMessage. |
curl https://nyami.cc/api/obfuscate/clx0a1b2c3d4e5f6g7h8i9j0 \
-H "X-API-Key: nyami_your_key_here"{
"jobId": "clx0a1b2c3d4e5f6g7h8i9j0",
"status": "COMPLETED",
"inputFileName": "script.py",
"inputFileSize": 4821,
"createdAt": "2026-08-15T10:04:11.204Z",
"startedAt": "2026-08-15T10:04:12.881Z",
"completedAt": "2026-08-15T10:04:39.552Z",
"outputFileSize": 261774,
"downloadUrl": "https://nyami.cc/api/obfuscate/clx0a1b2c3d4e5f6g7h8i9j0/download?token=..."
}Both PENDING and PROCESSING count against the concurrent job ceiling for your plan, so a slow poll from a long-running pipeline still occupies a slot until the job reaches a terminal state.
The downloadUrl on a COMPLETED job is a signed URL served by GET /api/obfuscate/{jobId}/download. The signed token authorises the request, and the URL is valid for one hour from the moment it is issued.
Treat the URL as a short-lived secret and do not cache it in a build log. If your pipeline holds the job id for longer than an hour, poll the job again to mint a fresh URL rather than storing the old one.
curl -L -o protected.py \
"https://nyami.cc/api/obfuscate/clx0a1b2c3d4e5f6g7h8i9j0/download?token=..."Limits apply per IP, per account and per key, and a credit is consumed in the same transaction that creates the job.
| Limit | Value |
|---|---|
| Input | One .py file per request, maximum 10 MB. |
| Submit rate, per IP | 60 requests per minute. |
| Submit rate, per account | 30 requests per minute. |
| Concurrent jobs | 2 on Pay as you go, 3 on standard dashboard plans, 5 on API plans. Counts PENDING and PROCESSING jobs together. |
| API keys | 5 active keys per account. The full key is shown once at creation. |
| Daily requests | 1000 requests per day per external key. |
| Download URL | Valid for one hour from the moment it is issued. |
Every failure returns a status and a message. The messages below are the exact strings the endpoint returns, so they are safe to match on.
| Status | Message | Cause |
|---|---|---|
| 400 | File required | The multipart body had no file field. |
| 400 | Only .py files are supported | The filename did not end in .py. |
| 400 | File too large (max 10MB) | The upload exceeded 10 MB. |
| 401 | Invalid or revoked API key | The X-API-Key header did not match a live key. |
| 401 | API key required or session expired | No API key was sent and no valid session cookie was present. |
| 402 | Insufficient credits. Please top up your account. | The plan's credit bucket is empty. |
| 403 | An active API subscription is required to use external API keys | The account holds no API bucket, or its term ended. |
| 404 | Job not found | The job id does not exist, or it belongs to another account. |
| 429 | Too many requests | The submit rate limit was hit. |
| 429 | Too many concurrent jobs (max N) | The account already has its plan's ceiling of PENDING or PROCESSING jobs. |
| 429 | Daily limit reached (1000/day) | The external key exhausted its daily quota. |
| 503 | Too many concurrent submissions on this account. Retry in a moment. | Another submission raced this one. The response sets Retry-After: 1. |
| 500 | Failed to submit file | The submit failed server side. |
A 402 means the credit bucket for your plan is empty. A 403 on an external key means the account does not hold an API plan. Both are account state rather than a problem with the request itself. A 429 about concurrency can arrive sooner on Pay as you go than on an API plan, because the ceiling differs by plan.
One script that submits, polls until the job completes, and downloads the artifact. It raises on a failed job rather than writing an empty file, and it uses only the endpoints and field names documented above.
import json
import time
import requests
API_KEY = "nyami_your_key_here"
BASE = "https://nyami.cc/api/obfuscate"
HEADERS = {"X-API-Key": API_KEY}
POLL_INTERVAL = 5
settings = {
"optimization": "1",
"python_version": "3.14",
"anti_debug": "none",
"lite_fobf": True,
"var_renaming": True,
"mixed_format": True,
"zip_light": True,
"windows_target": True,
"func_obf": True,
"kod": False,
"wif": False,
"no_console": False,
"pyinstaller": False,
"pytoc": False,
"zip_pyc": False,
"drv": False,
"debug": False,
"hwid": "",
"trial_time": "",
}
def submit(path):
with open(path, "rb") as handle:
response = requests.post(
BASE,
headers=HEADERS,
files={"file": (path, handle, "text/x-python")},
data={"settings": json.dumps(settings)},
timeout=120,
)
response.raise_for_status()
return response.json()["jobId"]
def wait_until_ready(job_id):
while True:
response = requests.get(f"{BASE}/{job_id}", headers=HEADERS, timeout=30)
response.raise_for_status()
job = response.json()
status = job["status"]
if status == "COMPLETED":
return job
if status == "FAILED":
detail = job.get("errorMessage", "no detail returned")
raise RuntimeError(f"job {job_id} failed: {detail}")
print(f"{job_id} is {status}")
time.sleep(POLL_INTERVAL)
def download(job, destination):
response = requests.get(job["downloadUrl"], timeout=300)
response.raise_for_status()
with open(destination, "wb") as out:
out.write(response.content)
return len(response.content)
if __name__ == "__main__":
job_id = submit("script.py")
print(f"submitted {job_id}")
# wait_until_ready raises on FAILED, so an empty artifact is never written.
job = wait_until_ready(job_id)
written = download(job, "protected.py")
print(f"wrote protected.py ({written} bytes)")The download happens only after the status reads COMPLETED, so a failed build raises instead of leaving a zero byte file on disk.