iSpy Bot API

Complete REST API reference for IMEI checks, order management, and service catalog.

v1 — Stable
$ https://api.ispy.services/api/v1

🔒 Authentication

All API requests require an API key passed via the X-API-Key header. You can manage your API keys in the Telegram bot under My Profile → API.

Request Header
$ curl -H "X-API-Key: sk_live_your_api_key" \
     https://api.ispy.services/api/v1/balance
Security: Never share your API key publicly. Treat it like a password. If compromised, regenerate it immediately via the bot.

Getting an API Key

1. Open @ispyware_bot in Telegram
2. Go to My Profile → API
3. Request API access or create a new key
4. Copy the sk_live_... key for your integrations

Rate Limits

Default rate limit: 100 requests per minute per API key. Rate limit status is returned in response headers.

HeaderDescription
X-RateLimit-LimitMaximum requests per window
X-RateLimit-RemainingRemaining requests in current window
X-RateLimit-ResetUnix timestamp when window resets
Note: If you exceed the rate limit you will receive a 429 Too Many Requests response. Contact the admin to request a higher limit for your key.

Endpoints

GET /health Health check

Check API server availability. Does not require authentication.

Request
$ curl https://api.ispy.services/health
Response · 200 OK
{
  "status": "ok",
  "version": "1.0.0"
}
GET /api/v1/balance Account balance

Returns the current balance and currency for the authenticated account.

Request
$ curl -H "X-API-Key: sk_live_xxx" \
     https://api.ispy.services/api/v1/balance
Response · 200 OK
{
  "success": true,
  "data": {
    "balance": 25.50,
    "currency": "USD"
  }
}
GET /api/v1/services List services

Returns the catalog of available check services with pricing.

Request
$ curl -H "X-API-Key: sk_live_xxx" \
     https://api.ispy.services/api/v1/services
Response · 200 OK
{
  "success": true,
  "data": {
    "services": [
      {
        "id": 1,
        "name": "IMEI Check Basic",
        "category": "IMEI",
        "price": 1.50,
        "input_types": ["imei", "serial"],
        "accept_file": false,
        "description": "Basic IMEI info: model, blacklist, FMI",
        "delivery_time": "instant"
      },
      {
        "id": 14,
        "name": "FMI ON/OFF [S1]",
        "category": "iCloud",
        "price": 0.02,
        "input_types": ["imei", "serial"],
        "accept_file": false,
        "description": "Find My iPhone ON/OFF check",
        "delivery_time": "instant"
      },
      {
        "id": 42,
        "name": "Test: File Echo",
        "category": "Tools",
        "price": 0.01,
        "input_types": ["file"],
        "accept_file": true,
        "description": "Upload a file, get it back renamed",
        "delivery_time": "instant"
      },
      {
        "id": 99,
        "name": "Custom Unlock Service",
        "category": "Unlock",
        "price": 5.00,
        "input_types": ["imei"],
        "accept_file": false,
        "description": "Unlock with extra parameters",
        "delivery_time": "1-24h",
        "required_params": [
          {"key": "username", "display_name": "Username", "input_type": "free_text", "options": [], "required": true},
          {"key": "usertype", "display_name": "User Type", "input_type": "select", "options": ["ExistingUser", "NewUser", "GuestUser"], "required": true}
        ]
      }
    ],
    "total": 53
  }
}
required_params: Some services require additional fields. When required_params is present and non-empty, pass these fields in the parameters object of POST /orders. For select type, the value must be one of the listed options.
POST /api/v1/orders Create order

Submit a new check order. The order cost is deducted from your balance automatically.

Request Body

ParameterTypeDescription
service_id required string ID of the service from /services (passed as string)
input_data required string IMEI (15 digits), serial number, or phone number depending on service
telegram_id required integer Your Telegram user ID (visible in bot under My Profile)
caption optional string Custom caption for file-based services. Displayed with the result file in Telegram. Max 1024 chars. Only used when the service has accept_file: true. See File-Based Services.
webhook_url optional string URL to receive result when order completes. Overrides the default webhook set on the API key. Must be http:// or https://, max 500 chars. See Webhooks.
parameters optional object Additional service fields as key-value pairs. Required when the service has required_params in GET /services. Example: {"username": "john", "usertype": "NewUser"}
Request
$ curl -X POST https://api.ispy.services/api/v1/orders \
     -H "X-API-Key: sk_live_xxx" \
     -H "Content-Type: application/json" \
     -d '{
  "service_id": "14",
  "input_data": "GW9F2X0R50",
  "telegram_id": 123456
}'
Response · 201 Created
{
  "success": true,
  "data": {
    "order_id": "7147",
    "status": "completed",
    "service_name": "FMI ON/OFF [S1]",
    "input_data": "GW9F2X0R50",
    "price": 0.02,
    "created_at": "2026-01-27T14:50:15Z"
  }
}
Note: Some services return results instantly (status: "completed"). Others require processing time and return status: "pending" — use GET /orders/{id} to poll, or configure a webhook.
Important: The input_data value is not validated at order creation time. The order will be accepted with status pending, and if the input is invalid, the worker will return status: "error" after processing. Check the final status via polling or webhook.
Error Response · 400 Validation Error
{
  "code": "VALIDATION_ERROR",
  "message": "Telegram ID is required",
  "localized_messages": {
    "en": "Telegram ID is required",
    "ru": "Ошибка валидации"
  }
}
GET /api/v1/orders/{id} Order status

Retrieve the status and result of a specific order.

Path Parameters

ParameterTypeDescription
id required string Order ID returned from POST /orders
Request
$ curl -H "X-API-Key: sk_live_xxx" \
     https://api.ispy.services/api/v1/orders/7147
Response · 200 OK (completed)
{
  "success": true,
  "data": {
    "order_id": "7147",
    "status": "completed",
    "service_name": "FMI ON/OFF [S1]",
    "input_data": "GW9F2X0R50",
    "output_data": {
      "find_my_watch": "ON"
    },
    "price": 0.02,
    "created_at": "2026-01-27T14:50:14Z",
    "updated_at": "2026-01-27T14:50:15Z"
  }
}
Response · 200 OK (pending)
{
  "success": true,
  "data": {
    "order_id": "7148",
    "status": "pending",
    "service_name": "GSX Full Report",
    "input_data": "353456789012345",
    "output_data": null,
    "price": 3.50,
    "created_at": "2026-01-27T15:12:00Z",
    "updated_at": "2026-01-27T15:12:00Z"
  }
}
GET /api/v1/orders List orders

List orders with filtering, sorting, and pagination.

Query Parameters

ParameterTypeDescription
status optionalstringpending | completed | error
service_id optionalintegerFilter by service ID
from_date optionalISO 8601Created from date, e.g. 2026-01-01T00:00:00Z
to_date optionalISO 8601Created to date
sort_by optionalstringcreated_at | updated_at | price
sort_dir optionalstringasc | desc (default: desc)
include_output optionalbooleanInclude output_data in response (default: true)
fields optionalstringComma-separated field names to return
page optionalintegerPage number (default: 1)
limit optionalintegerItems per page (default: 20, max: 100)
Request
$ curl -H "X-API-Key: sk_live_xxx" \
     "https://api.ispy.services/api/v1/orders?status=completed&limit=2&sort_by=created_at&sort_dir=desc"
Response · 200 OK
{
  "success": true,
  "data": {
    "orders": [
      {
        "order_id": "7147",
        "status": "completed",
        "service_name": "FMI ON/OFF [S1]",
        "input_data": "GW9F2X0R50",
        "output_data": { "find_my_watch": "ON" },
        "price": 0.02,
        "created_at": "2026-01-27T14:50:14Z"
      },
      {
        "order_id": "7140",
        "status": "completed",
        "service_name": "BlackList Check",
        "input_data": "353456789012345",
        "output_data": { "blacklisted": "Clean" },
        "price": 0.05,
        "created_at": "2026-01-27T12:30:00Z"
      }
    ],
    "total": 5184,
    "page": 1,
    "limit": 2
  }
}

📎 File-Based Services

Some services accept file input (documents, photos) instead of IMEI/serial numbers. These services are marked with accept_file: true in the GET /services response and include "file" in input_types.

How File Input Works

File-based services use Telegram file IDs as input. When a user sends a document or photo to the bot, Telegram assigns a unique file_id. This ID is passed in input_data using the following format:

FormatDescription
file:document:<file_id> Document file (PDF, XLSX, TXT, etc.)
file:photo:<file_id> Photo file (JPG, PNG)

Creating a File Order via API

Request · POST /api/v1/orders
$ curl -X POST https://api.ispy.services/api/v1/orders \
     -H "X-API-Key: sk_live_xxx" \
     -H "Content-Type: application/json" \
     -d '{
  "service_id": "42",
  "input_data": "file:document:BQACAgIAAxkBAAISkWnByZzcVaPB...",
  "telegram_id": 123456,
  "caption": "Process this invoice"
}'

Request Parameters for File Services

ParameterTypeDescription
input_data required string file:document:<file_id> or file:photo:<file_id>. The Telegram file ID of the uploaded file.
caption optional string Custom caption displayed with the result file in Telegram. Max 1024 characters. If omitted, a default localized message is used.

File Order Response

When a file-based service completes, the response includes a file_name field with the name of the result file.

GET /api/v1/orders/7200 · completed file order
{
  "success": true,
  "data": {
    "order_id": "7200",
    "status": "completed",
    "service_name": "Test: File Echo",
    "input_data": "file:document:BQACAgIAAxkB...",
    "output_data": "File received: file_0.pdf (413883 bytes), renamed to ok.txt",
    "file_name": "ok.txt",
    "price": 0.01,
    "created_at": "2026-03-24T10:00:00Z",
    "updated_at": "2026-03-24T10:00:02Z"
  }
}

Webhook Payload for File Orders

Webhook notifications for file orders include the file_name field:

Webhook POST Body · file order completed
{
  "order_id": "7200",
  "status": "completed",
  "input_data": "file:document:BQACAgIAAxkB...",
  "output_data": "File received: file_0.pdf, renamed to ok.txt",
  "file_name": "ok.txt",
  "price": 0.01,
  "completed_at": "2026-03-24T10:00:02Z"
}

Identifying File Services

Tip: Use the GET /services endpoint and filter by accept_file: true to find all file-accepting services. These services will also have "file" in the input_types array.
Important: File IDs are specific to the Telegram bot. A file_id obtained from one bot cannot be used with another. The file must have been previously sent to @ispyware_bot.

📦 Webhooks

Get notified via HTTP when an API order completes or fails — no polling required. Webhooks are sent only for orders created through the API (source = api); Telegram notifications are not sent for API orders.

Setting the Webhook URL

There are two ways to specify where the result is delivered. Per-request URL takes priority.

MethodPriorityDescription
Per-request Higher Pass webhook_url in the POST /api/v1/orders body. Applies only to that order.
Default (API key) Lower Set once in the Telegram bot: My Profile → API → select key → Webhook URL. Used when the request body has no webhook_url.
Tip: Use the default webhook on your API key for a "set and forget" setup. Override with webhook_url per request when you need results routed to a different endpoint.

Delivery Details

PropertyValue
HTTP methodPOST
Content-Typeapplication/json
Timeout30 seconds
SuccessAny 2xx status code
TriggerOrder completes (completed) or fails (error)
DeliveryAsynchronous — does not block order processing

Payload

Webhook POST Body · order completed
{
  "order_id": "7147",
  "status": "completed",
  "input_data": "GW9F2X0R50",
  "output_data": {
    "find_my_watch": "ON"
  },
  "price": 0.02,
  "completed_at": "2026-01-27T14:50:15Z"
}
Webhook POST Body · order failed
{
  "order_id": "7148",
  "status": "error",
  "input_data": "353456789012345",
  "output_data": null,
  "price": 3.50,
  "completed_at": "2026-01-27T15:14:22Z"
}

Payload Fields

FieldTypeDescription
order_idstringUnique order identifier — use for idempotency (duplicates possible on network retries)
statusstringcompleted or error
input_datastringIMEI / serial / phone submitted with the order
output_dataobject | nullCheck result. null when status is error
file_namestring | nullResult file name for file-based services (e.g. "ok.txt"). Omitted for non-file services. See File-Based Services
pricenumberAmount charged for this order (USD)
completed_atISO 8601Timestamp when the order was finalized

Example: Creating an Order with Webhook

Request · POST /api/v1/orders
$ curl -X POST https://api.ispy.services/api/v1/orders \
     -H "X-API-Key: sk_live_xxx" \
     -H "Content-Type: application/json" \
     -d '{
  "service_id": "14",
  "input_data": "GW9F2X0R50",
  "telegram_id": 123456,
  "webhook_url": "https://example.com/ispy-webhook"
}'
Response · 201 Created (order accepted, result will arrive at webhook)
{
  "success": true,
  "data": {
    "order_id": "7150",
    "status": "pending",
    "price": 0.02,
    "created_at": "2026-01-27T16:00:00Z"
  }
}

Signature Verification

Every webhook POST includes the header X-Webhook-Signature: sha256=<hex>, where <hex> is the HMAC-SHA256 of the raw request body using your API key as the secret. Always verify this header before trusting the payload — anyone with the URL could otherwise post fake results. Use a constant-time comparison (hmac.compare_digest / crypto.timingSafeEqual) to prevent timing attacks.

Node.js (Express)
const crypto = require('crypto');
const API_KEY = process.env.ISPY_API_KEY;

app.post('/ispy-webhook', express.raw({ type: 'application/json' }), (req, res) => {
  const header = req.headers['x-webhook-signature'] || '';
  const expected = 'sha256=' + crypto.createHmac('sha256', API_KEY).update(req.body).digest('hex');
  if (header.length !== expected.length ||
      !crypto.timingSafeEqual(Buffer.from(header), Buffer.from(expected))) {
    return res.status(401).send('bad signature');
  }
  const body = JSON.parse(req.body.toString('utf8'));
  // ... process body.order_id / body.status ...
  res.sendStatus(200);
});
Python (Flask)
import hmac, hashlib, os
from flask import request, abort

API_KEY = os.environ['ISPY_API_KEY'].encode()

@app.route('/ispy-webhook', methods=['POST'])
def ispy_webhook():
    header = request.headers.get('X-Webhook-Signature', '')
    expected = 'sha256=' + hmac.new(API_KEY, request.get_data(), hashlib.sha256).hexdigest()
    if not hmac.compare_digest(header, expected):
        abort(401)
    payload = request.get_json(force=True)
    # ... process payload['order_id'] / payload['status'] ...
    return '', 200
Note: The HMAC secret is your API key's plaintext value (the same sk_live_… / sk_test_… string you send in X-API-Key). Use the key whose default webhook URL matches this order's URL; if multiple keys share the user, any of their plain values will also verify. Keep the key secret — rotating the key rotates the HMAC secret.

Limitations

Bulk orders: Webhooks are currently supported for single API orders only. Bulk orders do not trigger webhook delivery.

• Only http:// and https:// URLs are accepted (max 500 characters).
• HTTPS is strongly recommended for production endpoints.
• The HTTP client timeout is 30 seconds; responses with 2xx status are treated as success.
• Duplicate deliveries are possible on network failures — use order_id for idempotent processing.

Managing the Default Webhook in Bot

In @ispyware_bot:
My Profile → API Access → select key
Webhook URL — set or update the default URL
Reset webhook — clear the URL (disables webhook for this key)

Error Codes

All error responses follow a consistent JSON format:

Error Response Format
{
  "code": "ERROR_CODE",
  "message": "Human-readable description",
  "localized_messages": {
    "en": "English message",
    "ru": "Сообщение на русском"
  }
}
HTTP CodeError CodeDescription
400 INVALID_REQUEST_BODY Malformed JSON or missing required fields
400 VALIDATION_ERROR Request body fields failed validation (e.g. missing telegram_id)
401 unauthorized Missing or invalid API key
402 insufficient_balance Not enough balance to complete the order
403 forbidden API key does not have permission for this action
404 not_found Order or service not found
422 validation_error Input data doesn't match service requirements
429 rate_limited Too many requests, try again later
500 internal_error Server error, contact support
502 upstream_error External provider returned an error
503 service_unavailable API is temporarily down for maintenance

💻 Code Examples

Quick-start examples for common integrations.

Full IMEI Check Flow

# 1. Check balance
$ curl -s -H "X-API-Key: sk_live_xxx" \
     https://api.ispy.services/api/v1/balance

# 2. List available services
$ curl -s -H "X-API-Key: sk_live_xxx" \
     https://api.ispy.services/api/v1/services

# 3. Create an order
$ curl -s -X POST https://api.ispy.services/api/v1/orders \
     -H "X-API-Key: sk_live_xxx" \
     -H "Content-Type: application/json" \
     -d '{"service_id": "14", "telegram_id": 123456, "input_data": "GW9F2X0R50"}'

# 4. Check order status (if pending)
$ curl -s -H "X-API-Key: sk_live_xxx" \
     https://api.ispy.services/api/v1/orders/7147
import requests
import time

API_KEY = "sk_live_xxx"
BASE    = "https://api.ispy.services/api/v1"
headers = {"X-API-Key": API_KEY}

# Check balance
balance = requests.get(f"{BASE}/balance", headers=headers).json()
print(f"Balance: {balance['data']['balance']} {balance['data']['currency']}")

# Create order
order = requests.post(f"{BASE}/orders", headers=headers, json={
    "service_id": "14",
    "telegram_id": 123456,
    "input_data": "GW9F2X0R50"
}).json()

order_id = order["data"]["order_id"]
print(f"Order {order_id}: {order['data']['status']}")

# Poll if pending
while order["data"]["status"] == "pending":
    time.sleep(5)
    order = requests.get(
        f"{BASE}/orders/{order_id}", headers=headers
    ).json()

print("Result:", order["data"]["output_data"])
const API_KEY = "sk_live_xxx";
const BASE    = "https://api.ispy.services/api/v1";

const headers = {
  "X-API-Key": API_KEY,
  "Content-Type": "application/json"
};

// Check balance
const bal = await fetch(`${BASE}/balance`, { headers }).then(r => r.json());
console.log(`Balance: ${bal.data.balance} ${bal.data.currency}`);

// Create order
const order = await fetch(`${BASE}/orders`, {
  method: "POST",
  headers,
  body: JSON.stringify({
    service_id: "14",
    telegram_id: 123456,
    input_data: "GW9F2X0R50"
  })
}).then(r => r.json());

console.log(`Order ${order.data.order_id}: ${order.data.status}`);

// Poll if pending
let result = order;
while (result.data.status === "pending") {
  await new Promise(r => setTimeout(r, 5000));
  result = await fetch(
    `${BASE}/orders/${order.data.order_id}`, { headers }
  ).then(r => r.json());
}

console.log("Result:", result.data.output_data);
<?php
$apiKey = 'sk_live_xxx';
$base   = 'https://api.ispy.services/api/v1';

function apiGet($url, $key) {
    $ch = curl_init($url);
    curl_setopt_array($ch, [
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_HTTPHEADER     => ["X-API-Key: {$key}"],
    ]);
    $res = curl_exec($ch);
    curl_close($ch);
    return json_decode($res, true);
}

function apiPost($url, $key, $data) {
    $ch = curl_init($url);
    curl_setopt_array($ch, [
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_POST           => true,
        CURLOPT_POSTFIELDS     => json_encode($data),
        CURLOPT_HTTPHEADER     => [
            "X-API-Key: {$key}",
            "Content-Type: application/json",
        ],
    ]);
    $res = curl_exec($ch);
    curl_close($ch);
    return json_decode($res, true);
}

// Check balance
$balance = apiGet("{$base}/balance", $apiKey);
echo "Balance: " . $balance['data']['balance'] . "\n";

// Create order
$order = apiPost("{$base}/orders", $apiKey, [
    'service_id' => '14',
    'telegram_id' => 123456,
    'input_data' => 'GW9F2X0R50',
]);

$orderId = $order['data']['order_id'];
echo "Order {$orderId}: " . $order['data']['status'] . "\n";

// Poll if pending
while ($order['data']['status'] === 'pending') {
    sleep(5);
    $order = apiGet("{$base}/orders/{$orderId}", $apiKey);
}

print_r($order['data']['output_data']);

Webhook Handler

from flask import Flask, request, jsonify

app = Flask(__name__)
processed = set()  # idempotency guard

@app.route("/ispy-webhook", methods=["POST"])
def handle_webhook():
    data = request.json
    order_id = data["order_id"]

    # Deduplicate (retries may send the same order_id)
    if order_id in processed:
        return jsonify({"ok": True}), 200
    processed.add(order_id)

    print(f"Order {order_id}: {data['status']}")
    print(f"  Input:  {data['input_data']}")
    print(f"  Price:  {data['price']}")

    if data["status"] == "completed":
        print(f"  Result: {data['output_data']}")
    else:
        print("  Order failed")

    return jsonify({"ok": True})

app.run(port=8080)
const express = require("express");
const app = express();
const processed = new Set();

app.use(express.json());

app.post("/ispy-webhook", (req, res) => {
  const { order_id, status, input_data, output_data, price } = req.body;

  // Deduplicate
  if (processed.has(order_id)) {
    return res.json({ ok: true });
  }
  processed.add(order_id);

  console.log(`Order ${order_id}: ${status}`);
  console.log(`  Input: ${input_data}, Price: ${price}`);

  if (status === "completed") {
    console.log("  Result:", output_data);
  } else {
    console.log("  Order failed");
  }

  res.json({ ok: true });
});

app.listen(8080);
<?php
// ispy-webhook.php
header('Content-Type: application/json');

$body = file_get_contents('php://input');
$data = json_decode($body, true);

if (!$data || empty($data['order_id'])) {
    http_response_code(400);
    echo json_encode(['error' => 'invalid payload']);
    exit;
}

$orderId = $data['order_id'];
$status  = $data['status'];

// TODO: check idempotency (e.g. DB lookup by order_id)

error_log("[webhook] Order {$orderId}: {$status}");

if ($status === 'completed') {
    $result = $data['output_data'];
    // Process result...
    error_log("[webhook] Result: " . json_encode($result));
}

echo json_encode(['ok' => true]);

Batch IMEI Check

Python
import requests, time

API_KEY = "sk_live_xxx"
BASE    = "https://api.ispy.services/api/v1"
headers = {"X-API-Key": API_KEY, "Content-Type": "application/json"}

imeis = ["353456789012345", "359876543210987", "GW9F2X0R50"]

# Submit all orders
order_ids = []
for imei in imeis:
    resp = requests.post(f"{BASE}/orders", headers=headers, json={
        "service_id": "14",
        "telegram_id": 123456,
        "input_data": imei
    }).json()
    if resp.get("success"):
        order_ids.append(resp["data"]["order_id"])
        print(f"Submitted {imei} -> order {resp['data']['order_id']}")

# Collect results
for oid in order_ids:
    for _ in range(20):
        r = requests.get(f"{BASE}/orders/{oid}", headers=headers).json()
        if r["data"]["status"] != "pending":
            print(f"Order {oid}: {r['data']['output_data']}")
            break
        time.sleep(3)

🔗 DHRU Fusion API Compatibility

iSpy Bot also exposes a DHRU Fusion API v6.1 compatible endpoint at POST /api/index.php. External DHRU panels can connect to iSpy as a standard DHRU provider.

Supported Actions

ActionDescription
imeiservicelistList available services (includes Requires.Custom for additional fields)
accountinfoAccount balance
placeimeiorderPlace an order (supports parameters XML with custom fields)
getimeiorderCheck order status

Requires.Custom Extension

Services with additional fields include a Requires object in the imeiservicelist response:

Requires.Custom format
"Requires": {
  "Custom": [
    {"fieldname": "Username", "fieldtype": "text", "description": "Username", "required": "on"},
    {"fieldname": "USERTYPE", "fieldtype": "dropdown", "description": "ExistingUser", "fieldoptions": "NewUser,GuestUser", "required": "on"}
  ]
}

When placing an order, pass custom fields in the parameters form field as XML: <Username>john</Username><USERTYPE>NewUser</USERTYPE><IMEI>353456789012345</IMEI>