API docs
The REST API powers the web app, the open-source Go CLI at github.com/oppa26/ioncube-decode, and any integration you want to build. Async pattern: upload → poll status → download.
Base URL
https://console.decodephp.ioAuthentication
All authenticated endpoints use the X-API-Key header. Your API key is your 16-digit account number, available in the console dashboard. API access is included for every account, and API decodes use the same credits as browser uploads.
X-API-Key: 1234567890123456Failed API-key attempts are rate-limited: 5 failures per minute per IP returns 429 Too Many Requests.
End-to-end flow
A credit-backed decode is three calls:
- Upload the file — returns a
decode_idand queues the job. - Poll status — returns
pending,success, orfailed. Poll roughly every 500 ms. - Download the decoded bytes when status is
success.
POST /accounts/upload/
Multipart form upload. The required file field must be a .php file up to 10 MB. An optional protection_file field accepts a requested companion file or matching SourceGuardian license, also up to 10 MB. Upload the file named in the error response. ionCube .icl licenses are not supported by this upload field.
curl https://console.decodephp.io/accounts/upload/ \
-H "X-API-Key: 1234567890123456" \
-F "file=@encoded.php"Response (credits available, decode queued):
{
"success": true,
"message": "File queued for decoding.",
"decode_id": 4821,
"task_id": "a8b2c9d4-1234-5678-abcd-ef0123456789"
}With no available credit, an eligible verified request is queued as a free preview. It normally returns 202; poll the same authenticated status endpoint with decode_id.
HTTP/1.1 202 Accepted
{
"success": false,
"code": "decode_pending",
"message": "Decoding is still in progress.",
"decode_id": 4821,
"task_id": "a8b2c9d4-1234-5678-abcd-ef0123456789"
}If the preview worker finishes during the upload request, the endpoint may instead return the 20-line preview immediately with 402. A balance race can also return 402and ask the client to retry after verification or purchase.
Protector and recovery failures use a stable code, a human-readable message, and optional structured details. Common HTTP responses include:
400 — Missing/wrong file, file too large, unrecognized or unsupported protector/PHP target
400 — Protection requirement such as a license, companion file, project key, or callback context
403 — Verification challenge required for an ordinary free preview
401 — Invalid API key
402 — No credit and preview could not be started on this request
429 — Rate limit (5 failed API keys / minute)GET /accounts/decode/<decode_id>/status/
Poll until status is success or failed.
curl https://console.decodephp.io/accounts/decode/4821/status/ \
-H "X-API-Key: 1234567890123456"Response while pending:
{
"success": true,
"decode_id": 4821,
"status": "pending",
"original_filename": "encoded.php",
"php_version": "8.4",
"ioncube_version": "15"
}Response on a credit-backed success:
{
"success": true,
"decode_id": 4821,
"status": "success",
"original_filename": "encoded.php",
"php_version": "8.4",
"ioncube_version": "15",
"decoded_size": 8912,
"processing_time_ms": 1420,
"download_url": "/accounts/decode/4821/download/",
"view_url": "/accounts/decode/4821/view/",
"opcodes_url": "/accounts/decode/4821/opcodes/"
}A free-preview success has status: "success", preview: true, preview_lines, and total_lines, but no download URL.
Response on failure:
{
"success": true,
"decode_id": 4821,
"status": "failed",
"original_filename": "encoded.php",
"php_version": "8.1",
"ioncube_version": "13",
"error_message": "Callback-key recovery needs more runtime context.",
"error_code": "callback_key_recovery_blocked",
"error_details": { "remaining_protected_sections": 2 },
"recovery_blocker": { "code": "callback_key_recovery_blocked" }
}The status response also exposes code and details aliases. A charged, viewable partial callback result may remain failed while including a view_url; that result consumes one credit.
GET /accounts/decode/<decode_id>/download/
Returns the decoded PHP source as application/x-php with a Content-Disposition: attachment header. Filename is <original>_decoded.php.
curl -OJ https://console.decodephp.io/accounts/decode/4821/download/ \
-H "X-API-Key: 1234567890123456"Error responses:
402 — Purchase credits to download (free-tier decode, preview only)
400 — Decoded file not available
404 — Not found / not owned by your accountGET /accounts/decode/<decode_id>/view/
Renders the decoded source in a browser viewer instead of downloading. Requires credits.
GET /accounts/decode/<decode_id>/opcodes/
Downloads the pretty-printed Zend opcode disassembly for the file. Useful for advanced reverse engineering. Requires credits.
GET /accounts/credits/
Current credit balance.
curl https://console.decodephp.io/accounts/credits/ \
-H "X-API-Key: 1234567890123456"{
"available_credits": 47,
"lifetime_remaining": null,
"lifetime_limit": null,
"purchased_credits": 47,
"is_lifetime": false,
"is_subscriber": false,
"subscription_remaining": null,
"subscription_limit": null
}POST /accounts/preview/ (public)
Public endpoint used by the landing-page demo. Send file, optional protection_file, and the Turnstile token required for ordinary public traffic. The normal response is 202 with an opaque preview_token and status_url. Poll that URL until it returns a 20-line eligible preview or a structured blocker. POST requests are limited to 30 per hour per IP.
{
"success": false,
"status": "pending",
"code": "decode_pending",
"preview_token": "opaque-uuid",
"status_url": "/accounts/preview/opaque-uuid/"
}The open-source Go CLI
The canonical reference implementation of this API is the Go CLI we publish on GitHub. It does upload, poll, download with a worker pool, and handles non-IonCube file skip detection.
# Install
go install github.com/oppa26/ioncube-decode@latest
# Single file
IONCUBE_API_KEY=1234567890123456 ioncube-decode encoded.php
# Directory (recursive), 8 concurrent workers, overwrite originals
IONCUBE_API_KEY=1234567890123456 ioncube-decode -w 8 -o ./src
Source: github.com/oppa26/ioncube-decode. MIT licensed.
Language examples
All examples assume DECODEPHP_KEY is your 16-digit API key.
PHP
<?php
$key = getenv('DECODEPHP_KEY');
$base = 'https://console.decodephp.io';
// 1. Upload
$ch = curl_init($base . '/accounts/upload/');
curl_setopt_array($ch, [
CURLOPT_HTTPHEADER => ['X-API-Key: ' . $key],
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => ['file' => new CURLFile('encoded.php')],
CURLOPT_RETURNTRANSFER => true,
]);
$uploaded = json_decode(curl_exec($ch), true);
$id = $uploaded['decode_id'];
// 2. Poll
while (true) {
$ch = curl_init($base . "/accounts/decode/{$id}/status/");
curl_setopt_array($ch, [
CURLOPT_HTTPHEADER => ['X-API-Key: ' . $key],
CURLOPT_RETURNTRANSFER => true,
]);
$s = json_decode(curl_exec($ch), true);
if ($s['status'] === 'success') break;
if ($s['status'] === 'failed') exit('decode failed: ' . $s['error_message']);
usleep(500000);
}
// 3. Download
$ch = curl_init($base . "/accounts/decode/{$id}/download/");
curl_setopt_array($ch, [
CURLOPT_HTTPHEADER => ['X-API-Key: ' . $key],
CURLOPT_RETURNTRANSFER => true,
]);
file_put_contents('decoded.php', curl_exec($ch));Python
import os, time, requests
key = os.environ['DECODEPHP_KEY']
base = 'https://console.decodephp.io'
headers = {'X-API-Key': key}
# 1. Upload
with open('encoded.php', 'rb') as f:
up = requests.post(
f'{base}/accounts/upload/',
headers=headers,
files={'file': f},
).json()
decode_id = up['decode_id']
# 2. Poll
while True:
s = requests.get(
f'{base}/accounts/decode/{decode_id}/status/',
headers=headers,
).json()
if s['status'] == 'success':
break
if s['status'] == 'failed':
raise RuntimeError(s['error_message'])
time.sleep(0.5)
# 3. Download
r = requests.get(
f'{base}/accounts/decode/{decode_id}/download/',
headers=headers,
)
with open('decoded.php', 'wb') as out:
out.write(r.content)Node.js
import fs from 'node:fs';
const key = process.env.DECODEPHP_KEY;
const base = 'https://console.decodephp.io';
const headers = { 'X-API-Key': key };
// 1. Upload
const form = new FormData();
form.append('file', new Blob([fs.readFileSync('encoded.php')]), 'encoded.php');
const upRes = await fetch(`${base}/accounts/upload/`, {
method: 'POST', headers, body: form,
});
const { decode_id } = await upRes.json();
// 2. Poll
while (true) {
const s = await (await fetch(
`${base}/accounts/decode/${decode_id}/status/`,
{ headers },
)).json();
if (s.status === 'success') break;
if (s.status === 'failed') throw new Error(s.error_message);
await new Promise(r => setTimeout(r, 500));
}
// 3. Download
const dl = await fetch(
`${base}/accounts/decode/${decode_id}/download/`,
{ headers },
);
fs.writeFileSync('decoded.php', Buffer.from(await dl.arrayBuffer()));Limits
- Maximum file size: 10 MB.
- Public preview: 30 requests / hour / IP.
- Failed API-key attempts: 5 / minute / IP.
- Concurrent decodes: bounded by your credit balance. The CLI default is 4 parallel workers — safe and fast.
Error codes
202 — Decode queued; poll the supplied decode/status URL
400 — Bad request or structured protector/recovery blocker
401 — Invalid API key
402 — Purchase credits to download, immediate free preview, or balance-race retry
403 — Verification challenge required
404 — Decode not found or not owned by your account
429 — Rate-limited
500/503 — Internal or queue error (retry with backoff)
504 — Processing timed out (opcode export only)