AseWeb Decoder
FOR DEVELOPERS

Two decoders.
One API.

Automate ionCube and SourceGuardian decoding with the same endpoints. Start with a free demo key. Activate a subscription for complete results.

BASE URLhttps://aseweb.ir/api/v1
01 / START

From key to decoded file

  1. Create an account or sign in, open Developer → Your API keys, name your key and copy it. The secret is shown once.
  2. Send Authorization: Bearer YOUR_API_KEY with every request. Keep it in a server environment variable.
  3. Upload an encoded PHP file or ZIP with a decoder selection.
  4. Poll the returned job URL until complete, partial or failed, or receive a signed webhook. Download the available result.

Use GET /me before uploading to check your current access and upload limits. A cookie login does not authenticate API endpoints. Cross-origin browser access is not enabled: call this API from your server.

# Store the key securely; do not paste it into source control.
export ASEWEB_API_KEY='YOUR_API_KEY'

curl --fail-with-body https://aseweb.ir/api/v1/me \
  -H "Authorization: Bearer $ASEWEB_API_KEY"

curl --fail-with-body https://aseweb.ir/api/v1/jobs \
  -H "Authorization: Bearer $ASEWEB_API_KEY" \
  -F 'decoder=ioncube' -F 'file=@encoded.php'

# Replace JOB_ID with job.id from the response. Poll at most once every 3 seconds.
curl --fail-with-body https://aseweb.ir/api/v1/jobs/JOB_ID \
  -H "Authorization: Bearer $ASEWEB_API_KEY"

# Only after status is complete or partial:
curl --fail-with-body https://aseweb.ir/api/v1/jobs/JOB_ID/download \
  -H "Authorization: Bearer $ASEWEB_API_KEY" -o result.php
02 / ACCESS

Your key follows your account

CapabilityFree / expired subscriptionActive subscription
Endpoints & decoder selectionSame endpoints, both decodersSame endpoints, both decoders
PHP outputFirst 15 formatted lines, up to 1,500 Unicode charactersFull reconstructed PHP
ZIP outputPreview of the first successfully decoded PHP file; no ZIP downloadComplete available ZIP with folder structure
JSON previewLimited prefix, truncated: trueFull first decoded PHP file, truncated: false
Decode creditsNot spentNot spent
Upload rate5 per hour per account; 20 per hour per IP60 per minute per account

Full API access is included with every subscription: 7 days for $24, 10 days for $29 or 30 days for $69. Buying file credits alone does not enable full API access. Choose a subscription →

The same key upgrades automatically when your plan activates and returns to demo access when it expires. A request submitted as a demo remains a demo after upgrading; submit the file again for full output. Full API downloads require an active plan, including downloads of previously full jobs. This does not change paid website downloads.

Use up to five active keys per account. Revoke a key from Developer to stop it immediately. Banned or deleted accounts cannot use keys. Treat the secret like a password; never put it in a URL, public frontend, screenshots or logs.

03 / REFERENCE

API endpoints

Every endpoint below includes the complete URL, headers, body and a ready-to-copy curl command. Use the same URLs with free and subscription keys.

Set your key once

Replace YOUR_API_KEY with the key from Developer → Your API keys. For job-specific calls, replace JOB_ID_FROM_RESPONSE with job.id returned by the upload. Keep the key on your server.

export ASEWEB_API_KEY="YOUR_API_KEY"
export ASEWEB_JOB_ID="JOB_ID_FROM_RESPONSE"
GET200 OK · JSON

Check your account and API access

Check whether this key currently has demo or full access, the subscription expiry, current upload limits and file retention.

Endpoint

https://aseweb.ir/api/v1/me

Headers

HeaderValueRequirement
AuthorizationBearer YOUR_API_KEYRequired
Acceptapplication/jsonRecommended

Body

None. This GET request has no body.

curl example

curl --fail-with-body --request GET \
  "https://aseweb.ir/api/v1/me" \
  --header "Authorization: Bearer ${ASEWEB_API_KEY}" \
  --header "Accept: application/json"

Response

200 OK · JSON

{
  "user": {
    "id": "0123456789abcdef0123456789abcdef",
    "email": "you@example.com"
  },
  "access": "demo",
  "subscriptionExpiresAt": null,
  "previewLimits": {
    "lines": 15,
    "characters": 1500,
    "zip": "first successfully decoded PHP file"
  },
  "uploadLimits": {
    "phpBytes": 2097152,
    "zipBytes": 20971520,
    "phpFilesPerZip": 1000
  },
  "retentionDays": 7
}

Example values; some responses show selected fields. See the OpenAPI specification for the complete schema.

POST202 Accepted · JSON

Upload a file to decode

Send one encoded PHP file or ZIP archive. The same URL supports ionCube and SourceGuardian; the decoder field selects the format. Your API key determines demo or full output.

Endpoint

https://aseweb.ir/api/v1/jobs

Headers

HeaderValueRequirement
AuthorizationBearer YOUR_API_KEYRequired
Acceptapplication/jsonRecommended
Content-Typemultipart/form-data; boundary=…Automatic — set by curl / your HTTP client

Body

multipart/form-data Send form fields, not a JSON body.

FieldValueRequirement
decoderioncube or sourceguardianRequired · string
fileAn encoded .php file or .zip archive.Required · binary file

Use curl’s --form option. It generates the Content-Type and boundary automatically; do not add a manual Content-Type header.

curl example

curl --fail-with-body --request POST \
  "https://aseweb.ir/api/v1/jobs" \
  --header "Authorization: Bearer ${ASEWEB_API_KEY}" \
  --header "Accept: application/json" \
  --form "decoder=ioncube" \
  --form "file=@./encoded.php"
SourceGuardian ZIP example
curl --fail-with-body --request POST \
  "https://aseweb.ir/api/v1/jobs" \
  --header "Authorization: Bearer ${ASEWEB_API_KEY}" \
  --header "Accept: application/json" \
  --form "decoder=sourceguardian" \
  --form "file=@./project.zip"

Save job.id from the response and use it in the status, preview and download URLs. HTTP 202 means accepted, even when the response already says complete. The Location and Retry-After headers are also returned. Upload POSTs are not idempotent: check history before retrying after a timeout.

Response

202 Accepted · JSON

{
  "job": {
    "id": "0123456789abcdef0123456789abcdef",
    "decoder": "ioncube",
    "originalName": "encoded.php",
    "status": "queued",
    "access": "demo",
    "submittedAccess": "demo",
    "links": {
      "self": "/api/v1/jobs/0123456789abcdef0123456789abcdef",
      "preview": "/api/v1/jobs/0123456789abcdef0123456789abcdef/preview",
      "download": "/api/v1/jobs/0123456789abcdef0123456789abcdef/download"
    }
  }
}

Example values; some responses show selected fields. See the OpenAPI specification for the complete schema.

GET200 OK · JSON

List your API decode history

Returns this account’s API submissions, newest first, in pages of 50. All keys for the same account share this history. Website-only jobs are not included.

Endpoint

https://aseweb.ir/api/v1/jobs

Headers

HeaderValueRequirement
AuthorizationBearer YOUR_API_KEYRequired
Acceptapplication/jsonRecommended

Query parameter

NameValueRequirement
cursorUse nextCursor from the previous page: https://aseweb.ir/api/v1/jobs?cursor=NEXT_CURSOR. Stop when nextCursor is null.Optional

Body

None. This GET request has no body.

curl example

curl --fail-with-body --request GET \
  "https://aseweb.ir/api/v1/jobs" \
  --header "Authorization: Bearer ${ASEWEB_API_KEY}" \
  --header "Accept: application/json"
Next-page curl example
curl --fail-with-body --get \
  "https://aseweb.ir/api/v1/jobs" \
  --header "Authorization: Bearer ${ASEWEB_API_KEY}" \
  --header "Accept: application/json" \
  --data-urlencode "cursor=NEXT_CURSOR"

Listing does not refresh every job. Use the individual status endpoint to update a pending request.

Response

200 OK · JSON

{
  "jobs": [
    {
      "id": "0123456789abcdef0123456789abcdef",
      "status": "complete",
      "access": "demo"
    }
  ],
  "nextCursor": null
}

Example values; some responses show selected fields. See the OpenAPI specification for the complete schema.

GET200 OK · JSON

Check a decode request

Poll this endpoint until the job is complete, partial or failed. Wait at least three seconds between polls; while queued, wait 15–30 seconds. Queue estimates are optional and may change.

Endpoint

https://aseweb.ir/api/v1/jobs/{id}

Headers

HeaderValueRequirement
AuthorizationBearer YOUR_API_KEYRequired
Acceptapplication/jsonRecommended

Path parameter

NameValueRequirement
idThe 32-character job.id returned by POST https://aseweb.ir/api/v1/jobs. Replace {id} in the URL.Required

Body

None. This GET request has no body.

curl example

curl --fail-with-body --request GET \
  "https://aseweb.ir/api/v1/jobs/${ASEWEB_JOB_ID}" \
  --header "Authorization: Bearer ${ASEWEB_API_KEY}" \
  --header "Accept: application/json"

Statuses: submitting, queued, decoding, complete, partial or failed. Inspect per-file results for a partial archive. Failed jobs return an error and no downloadable result.

Response

200 OK · JSON

{
  "job": {
    "id": "0123456789abcdef0123456789abcdef",
    "decoder": "ioncube",
    "originalName": "encoded.php",
    "status": "complete",
    "access": "demo",
    "submittedAccess": "demo",
    "error": null,
    "downloadable": true,
    "estimatedWait": null
  }
}

Example values; some responses show selected fields. See the OpenAPI specification for the complete schema.

GET200 OK · JSON

Preview the decoded source

Returns decoded PHP as JSON. Demo access returns up to 15 formatted lines and 1,500 Unicode characters. Full access returns the complete selected PHP file. For ZIPs, the first successfully decoded PHP file is selected.

Endpoint

https://aseweb.ir/api/v1/jobs/{id}/preview

Headers

HeaderValueRequirement
AuthorizationBearer YOUR_API_KEYRequired
Acceptapplication/jsonRecommended

Path parameter

NameValueRequirement
idThe 32-character job.id returned by POST https://aseweb.ir/api/v1/jobs. Replace {id} in the URL.Required

Body

None. This GET request has no body.

curl example

curl --fail-with-body --request GET \
  "https://aseweb.ir/api/v1/jobs/${ASEWEB_JOB_ID}/preview" \
  --header "Authorization: Bearer ${ASEWEB_API_KEY}" \
  --header "Accept: application/json"

HTTP 409 means the output is not available yet; check job status. Demo prefixes are intentionally incomplete and may end inside a statement. truncated: true always marks demo output.

Response

200 OK · JSON

{
  "source": "<?php\necho \"Example\";",
  "outputName": "encoded-preview.php",
  "access": "demo",
  "truncated": true,
  "totalLines": 180,
  "expiresAt": 1794182400000
}

Example values; some responses show selected fields. See the OpenAPI specification for the complete schema.

GET200 OK · file attachment

Download the decoded result

Returns an attachment. Full access downloads the complete available PHP file or ZIP. Demo access downloads only a plain-text PHP preview, including when the uploaded file was a ZIP.

Endpoint

https://aseweb.ir/api/v1/jobs/{id}/download

Headers

HeaderValueRequirement
AuthorizationBearer YOUR_API_KEYRequired
Accept*/*Recommended

Path parameter

NameValueRequirement
idThe 32-character job.id returned by POST https://aseweb.ir/api/v1/jobs. Replace {id} in the URL.Required

Body

None. This GET request has no body.

curl example

curl --fail-with-body --request GET \
  "https://aseweb.ir/api/v1/jobs/${ASEWEB_JOB_ID}/download" \
  --header "Authorization: Bearer ${ASEWEB_API_KEY}" \
  --header "Accept: */*" \
  --output result.php

The command saves result.php. For a full decoded ZIP, change it to --output result.zip. Check Content-Disposition for the output filename and X-AseWeb-Access: demo or full for the response access. HTTP 409 means not ready; HTTP 410 means the file expired.

Response

200 OK · file attachment

A PHP or ZIP file stream for full access, or a text preview for demo access. The response is not JSON.

GET200 OK · JSON

Inspect webhook delivery

See the notification state and every delivery attempt for an API job owned by your account. The callback URL and private delivery fields are never returned.

Endpoint

https://aseweb.ir/api/v1/jobs/{id}/webhook

Headers

HeaderValueRequirement
AuthorizationBearer YOUR_API_KEYRequired
Acceptapplication/jsonRecommended

Path parameter

id: the 32-character job ID returned by your upload.

Body

None. This GET request has no body.

curl example

curl --fail-with-body \
  "https://aseweb.ir/api/v1/jobs/${ASEWEB_JOB_ID}/webhook" \
  --header "Authorization: Bearer ${ASEWEB_API_KEY}"

Response

{
  "webhook": {
    "eventId": "abcdef0123456789abcdef0123456789",
    "status": "pending",
    "attempts": 1,
    "maxAttempts": 8,
    "nextAttemptAt": 1794139260000,
    "lastHttpStatus": 503,
    "lastError": "Receiver returned HTTP 503",
    "deliveredAt": null,
    "createdAt": 1794139200000,
    "updatedAt": 1794139200000
  },
  "attempts": [{
    "attempt": 1,
    "startedAt": 1794139200000,
    "finishedAt": 1794139200000,
    "httpStatus": 503,
    "error": "Receiver returned HTTP 503"
  }]
}

States: waiting for decode completion, pending for delivery/retry, delivered, exhausted or cancelled. A null HTTP status means no response was received. An unfinished attempt has a null finish time. HTTP 404 means the job is not yours or no webhook was enabled for this submission. Upload and single-job status responses also include job.webhook (null when absent).

04 / INTEGRATE

Working examples

Python · SourceGuardian PHP

# pip install requests
import os, time
import requests

base = "https://aseweb.ir/api/v1"
session = requests.Session()
session.headers["Authorization"] = "Bearer " + os.environ["ASEWEB_API_KEY"]

with open("encoded.php", "rb") as file:
    response = session.post(base + "/jobs", data={"decoder": "sourceguardian"},
                            files={"file": file}, timeout=120)
response.raise_for_status()
job = response.json()["job"]
while job["status"] not in ("complete", "partial", "failed"):
    time.sleep(5 if job["status"] != "queued" else 15)
    response = session.get(base + "/jobs/" + job["id"], timeout=60)
    response.raise_for_status()
    job = response.json()["job"]
if job["status"] == "failed":
    raise RuntimeError(job.get("error") or "Decode failed")

response = session.get(base + "/jobs/" + job["id"] + "/preview", timeout=60)
response.raise_for_status()
print(response.json()["source"])
response = session.get(base + "/jobs/" + job["id"] + "/download", timeout=120)
response.raise_for_status()
with open("result.php", "wb") as file:  # use result.zip for full ZIP uploads
    file.write(response.content)

JavaScript · ionCube ZIP

// Node.js 22+, run on your server. No extra packages.
import { readFile, writeFile } from "node:fs/promises";
const base = "https://aseweb.ir/api/v1";
const headers = { Authorization: `Bearer ${process.env.ASEWEB_API_KEY}` };
async function call(path, options = {}) {
  const response = await fetch(base + path, { ...options, headers });
  if (!response.ok) throw new Error(`${response.status}: ${await response.text()}`);
  return response;
}
const form = new FormData();
form.set("decoder", "ioncube");
form.set("file", new File([await readFile("project.zip")], "project.zip"));
let { job } = await (await call("/jobs", { method: "POST", body: form })).json();
while (!["complete", "partial", "failed"].includes(job.status)) {
  await new Promise(resolve => setTimeout(resolve, job.status === "queued" ? 15000 : 5000));
  ({ job } = await (await call(`/jobs/${job.id}`)).json());
}
if (job.status === "failed") throw new Error(job.error || "Decode failed");
const output = await call(`/jobs/${job.id}/download`);
const filename = output.headers.get("x-aseweb-access") === "demo" ? "preview.php" : "decoded.zip";
await writeFile(filename, Buffer.from(await output.arrayBuffer()));

Neither example retries submission automatically. Handle timeouts by checking your job history, and handle 429 responses with a delay. Keep long-running polling within your own application’s timeout policy.

05 / NOTIFICATIONS

Just save your webhook URL

No webhook secret or signature setup is needed. Save your endpoint once and receive a JSON notification when your API decode finishes.

  1. Open Developer → Webhook, enter your HTTPS URL and click Save webhook.
  2. Click Test webhook to check delivery. You’ll see its status, HTTP result, time and any error. Tests use no credits.
  3. Upload files normally. Future API uploads automatically use your saved URL, across all your API keys and both decoders.

Clear the URL and save to stop notifications. Changing or removing it cancels pending deliveries to the previous URL. No callback fields are needed in upload requests.

Upload normally

curl --fail-with-body https://aseweb.ir/api/v1/jobs \
  --header "Authorization: Bearer ${ASEWEB_API_KEY}" \
  --form 'decoder=ioncube' \
  --form 'file=@encoded.php'

Use decoder=sourceguardian for SourceGuardian or file=@project.zip for an archive. GET https://aseweb.ir/api/v1/me includes webhooks.configured and webhooks.enabled. Individual job responses include delivery state under job.webhook.

What your URL receives

AseWeb sends an HTTP POST with Content-Type: application/json when the job becomes complete, partial or failed, including queued completions. A ZIP produces one summary notification; use job status for individual files.

{
  "id": "abcdef0123456789abcdef0123456789",
  "type": "decode.complete",
  "createdAt": 1794139200000,
  "data": {
    "id": "0123456789abcdef0123456789abcdef",
    "status": "complete",
    "decoder": "ioncube",
    "originalName": "encoded.php",
    "submittedAccess": "full",
    "expiresAt": 1794744000000,
    "fileCount": 1,
    "successCount": 1,
    "failedCount": 0,
    "error": null,
    "links": {
      "self": "https://aseweb.ir/api/v1/jobs/0123456789abcdef0123456789abcdef",
      "preview": "https://aseweb.ir/api/v1/jobs/0123456789abcdef0123456789abcdef/preview",
      "download": "https://aseweb.ir/api/v1/jobs/0123456789abcdef0123456789abcdef/download"
    }
  }
}

The event types are decode.complete, decode.partial and decode.failed. Manual tests use webhook.test with data.test: true and a short message. A test ID is not a decode job ID and has no downloadable file.

Notifications contain status and authenticated download links. Downloads continue to follow the account’s API access: demo output for free accounts and complete results for active subscriptions. An API key is still required to fetch a result.

Simple receiver example

This Node.js 22.13+ example accepts JSON, saves each event once in SQLite and returns HTTP 202. Test events are acknowledged without queuing a download. Put it behind HTTPS and save its public URL in Developer. Download the example →

// Node.js 22.13+. Put behind your HTTPS reverse proxy on port 443.
// Save https://your-domain.com/webhooks/aseweb in AseWeb's Developer panel.
import { createServer } from 'node:http';
import { DatabaseSync } from 'node:sqlite';

const db = new DatabaseSync(process.env.WEBHOOK_DB || 'webhooks.sqlite');
db.exec(`CREATE TABLE IF NOT EXISTS received_events (
  id TEXT PRIMARY KEY, body TEXT NOT NULL, received_at INTEGER NOT NULL,
  processed_at INTEGER
)`);
const enqueue = db.prepare('INSERT OR IGNORE INTO received_events(id,body,received_at) VALUES(?,?,?)');
const enqueueTest = db.prepare('INSERT OR IGNORE INTO received_events(id,body,received_at,processed_at) VALUES(?,?,?,?)');

createServer(async (req, res) => {
  if (req.method !== 'POST' || req.url !== '/webhooks/aseweb') {
    res.writeHead(404).end(); return;
  }
  try {
    const chunks = []; let size = 0;
    for await (const chunk of req) {
      size += chunk.length;
      if (size > 65536) { res.writeHead(413).end(); return; }
      chunks.push(chunk);
    }
    const raw = Buffer.concat(chunks).toString('utf8');
    let event;
    try { event = JSON.parse(raw); }
    catch { res.writeHead(400).end(); return; }
    if (!event || !/^[a-f0-9]{32}$/.test(event.id) ||
        !['decode.complete', 'decode.partial', 'decode.failed', 'webhook.test'].includes(event.type) ||
        !/^[a-f0-9]{32}$/.test(event.data?.id)) {
      res.writeHead(400).end(); return;
    }
    // Save each event once before acknowledging it. Your worker can confirm
    // decode status and download files through the authenticated AseWeb API.
    const now = Date.now();
    if (event.type === 'webhook.test') enqueueTest.run(event.id, raw, now, now);
    else enqueue.run(event.id, raw, now);
    res.writeHead(202).end();
  } catch {
    // Storage failures must not acknowledge delivery; AseWeb can then retry.
    res.writeHead(500).end();
  }
}).listen(Number(process.env.PORT || 3000), '127.0.0.1', () => {
  console.log('Webhook receiver ready');
});
node webhook-receiver.mjs

Callbacks are unsigned notifications. Use the authenticated job-status endpoint for authoritative status before processing or downloading a result. The event ID lets your receiver handle duplicate deliveries.

Delivery and retries

  • Return any HTTP 2xx response within 10 seconds to acknowledge delivery.
  • Network/DNS failures, timeouts and non-2xx responses trigger retries. Decode notifications have up to 8 attempts total, with retry delays of approximately 1 minute, 5 minutes, 15 minutes, 1 hour, 3 hours, 6 hours and 12 hours after the preceding failed attempt.
  • Queued completions and due retries are checked every minute; load can delay dispatch. The same event ID and JSON body are used for retries. Delivery may happen before the upload response reaches you.
  • Manual tests make one delivery attempt, with no automatic retry. Limits are three tests per minute and ten per hour per account. The latest test result survives page refreshes.
  • Only public HTTPS hostnames on port 443 are accepted. Redirects, private destinations, IP literals, credentials in URLs and AseWeb service URLs are blocked.
  • HTTP-rejected submissions do not trigger completion notifications. A webhook failure does not change the decode result; polling and authenticated downloads remain available.
  • Delivery is sent through Cloudflare and has no fixed dedicated sender IP. Informational headers include User-Agent: AseWeb-Webhooks/1.0, X-AseWeb-Event-ID, X-AseWeb-Timestamp (Unix seconds) and X-AseWeb-Attempt. JSON timestamps use Unix milliseconds.
  • Inspect delivery state and attempts at GET https://aseweb.ir/api/v1/jobs/{id}/webhook. Administrators can inspect decode notifications and manual tests under Webhooks.
06 / OPERATE

Limits, retention & errors

  • Use GET /me for the live upload limits. Defaults: 2 MB per PHP file, 20 MB per ZIP and 1,000 PHP files per ZIP. Administrators can change these.
  • 120 API requests per minute per account across all keys, plus the upload rates above. HTTP 429 means the current minute/hour window is full. Wait for the window to reset.
  • Files and preview text expire after seven days; history remains. Expired output returns HTTP 410. Download promptly.
  • No endpoint executes your PHP. Results are reconstructed source; comments, original formatting and some original names may be absent. Compatibility varies by protected file. Test the result in your own environment.
  • Readable PHP, wrong-decoder uploads, unknown binaries and invalid archives are rejected. ZIPs containing no decodable files cannot return a demo. Files needing unavailable protection keys may require their companion files in the same ZIP.
  • Decoded archives may be partial. Check status and the file list before assuming every member succeeded. Demo ZIP previews do not expose a complete per-file report.
  • Only your account’s API jobs are accessible. A revoked key, banned account or deleted account returns HTTP 401. This API does not expose admin or billing operations.
HTTPMeaning / action
202Upload accepted. Read job.status, then poll or wait for your configured webhook.
400Invalid input, unsupported file or wrong decoder. Correct the upload.
401Missing/invalid/revoked key or inactive account.
403Access denied by an IP ban (code: ip_banned), or a subscription changed during upload. For an IP ban, contact support; otherwise recheck /me.
404Unknown route or job not owned by this account.
409Output not ready or submission still being accepted. Poll status.
410File/preview expired. Submit a fresh request.
413 / 415Upload too large / wrong content type. Use multipart/form-data and live limits.
429Rate limited. Back off; do not generate more keys to bypass limits.
500 / 503Server error / temporarily unavailable. Retry status reads later; check history before retrying an upload.
{"error":"Decoded output is not available"}

Responses include X-Request-ID. Share that ID with @amirmalek0 for support; never send your API key. Keep response bodies and keys out of public logs.