Nyami
FeaturesPricing

Getting started

  • Overview
  • Quickstart

Reference

  • Settings
  • API

Protection

  • Security model
  • Licensing

Help

  • Troubleshooting

Docs / API

API reference

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.

Authentication

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.

Submit a job

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.

FieldTypeRequiredNotes
filemultipart partRequiredThe .py source to protect. Maximum 10 MB.
settingsJSON stringRequiredThe settings object serialised as a JSON string. Any key you leave out falls back to its default.
X-API-Keyrequest headerRequired for external keysThe nyami_... key issued to your account. Dashboard requests carry an internal key instead.
Request
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"}'
200 response
{
  "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.

Poll a job

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.

FieldTypeNotes
jobIdstringThe identifier of the job.
statusstringPENDING, PROCESSING, COMPLETED or FAILED.
inputFileNamestringThe name of the file you submitted.
inputFileSizenumberThe size of the submitted file in bytes.
createdAtstringWhen the job was created.
startedAtstringWhen the pipeline picked the job up.
completedAtstringWhen the job reached a terminal state.
outputFileSizenumberThe size of the artifact in bytes. Present on a COMPLETED job.
downloadUrlstringA signed URL for the artifact. Present on a COMPLETED job and valid for one hour.
errorMessagestringThe failure detail. Present on a FAILED job in place of the output fields.

status takes one of four values.

StatusMeaning
PENDINGThe job is queued and has not started. It is counted against your plan's concurrent job ceiling.
PROCESSINGThe pipeline is running. It is still counted against the ceiling.
COMPLETEDThe artifact is ready. The response carries outputFileSize and a signed downloadUrl.
FAILEDThe job ended without an artifact. The response carries errorMessage.
Request
curl https://nyami.cc/api/obfuscate/clx0a1b2c3d4e5f6g7h8i9j0 \
  -H "X-API-Key: nyami_your_key_here"
200 response (COMPLETED)
{
  "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.

Download the artifact

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.

Download
curl -L -o protected.py \
  "https://nyami.cc/api/obfuscate/clx0a1b2c3d4e5f6g7h8i9j0/download?token=..."

Limits

Limits apply per IP, per account and per key, and a credit is consumed in the same transaction that creates the job.

LimitValue
InputOne .py file per request, maximum 10 MB.
Submit rate, per IP60 requests per minute.
Submit rate, per account30 requests per minute.
Concurrent jobs2 on Pay as you go, 3 on standard dashboard plans, 5 on API plans. Counts PENDING and PROCESSING jobs together.
API keys5 active keys per account. The full key is shown once at creation.
Daily requests1000 requests per day per external key.
Download URLValid for one hour from the moment it is issued.

Error codes

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.

StatusMessageCause
400File requiredThe multipart body had no file field.
400Only .py files are supportedThe filename did not end in .py.
400File too large (max 10MB)The upload exceeded 10 MB.
401Invalid or revoked API keyThe X-API-Key header did not match a live key.
401API key required or session expiredNo API key was sent and no valid session cookie was present.
402Insufficient credits. Please top up your account.The plan's credit bucket is empty.
403An active API subscription is required to use external API keysThe account holds no API bucket, or its term ended.
404Job not foundThe job id does not exist, or it belongs to another account.
429Too many requestsThe submit rate limit was hit.
429Too many concurrent jobs (max N)The account already has its plan's ceiling of PENDING or PROCESSING jobs.
429Daily limit reached (1000/day)The external key exhausted its daily quota.
503Too many concurrent submissions on this account. Retry in a moment.Another submission raced this one. The response sets Retry-After: 1.
500Failed to submit fileThe 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.

A complete client

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.

client.py
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.

Previous

Settings

Next

Security model

NYAMI

Python protection through compilation, encryption, and active defense.

40+ protection modules

Product

  • Features
  • Pricing
  • Comparison
  • Purchase

Developers

  • Quickstart
  • Documentation
  • Blog

Support

  • Discord
  • projectnyami@proton.me

© 2026 Nyami. All rights reserved.

Terms of ServicePrivacy PolicyRefund Policy
NYAMI